Skip to content

Commit aaf71e6

Browse files
committed
Merge branch 'master' into piper
2 parents 1c189bf + aa9276f commit aaf71e6

File tree

8 files changed

+125
-96
lines changed

8 files changed

+125
-96
lines changed

Diff for: .github/workflows/main.yml

+10-10
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,18 @@ jobs:
1515

1616
strategy:
1717
matrix:
18-
python-version: ["3.9", "3.10", "3.11", "3.12", "pypy-3.9"]
18+
python-version: ["3.9", "3.12", "pypy-3.10"]
1919

2020
steps:
2121
- uses: actions/checkout@v4
2222
- name: Set up Python ${{ matrix.python-version }}
23-
uses: actions/setup-python@v4
23+
uses: actions/setup-python@v5
2424
with:
2525
python-version: ${{ matrix.python-version }}
2626
- name: Install dependencies
2727
run: |
2828
python -m pip install --upgrade pip wheel setuptools
29-
pip install -e '.[dev]'
29+
python -m pip install -e '.[dev]'
3030
- name: Set up API keys
3131
run: |
3232
# Azure TTS key
@@ -35,19 +35,19 @@ jobs:
3535
echo '${{ secrets.AWS_POLLY_KEY }}' > keys/AWSPollyServerKey.json
3636
- name: Test with pytest
3737
run: |
38-
pytest --run-slow -vvvrP --log-level=DEBUG --capture=tee-sys
39-
- name: Lint with pre-commit hooks
40-
run: |
41-
pre-commit run --all-files
38+
python -m pytest --run-slow -vvvrP --log-level=DEBUG --capture=tee-sys
39+
# - name: Lint with pre-commit hooks
40+
# run: |
41+
# pre-commit run --all-files
4242

4343
network:
4444
runs-on: ubuntu-22.04
4545
steps:
4646
- uses: actions/checkout@v4
47-
- name: Set up Python 3.10
48-
uses: actions/setup-python@v4
47+
- name: Set up Python 3.12
48+
uses: actions/setup-python@v5
4949
with:
50-
python-version: '3.10'
50+
python-version: '3.12'
5151
- name: Install dependencies
5252
run: |
5353
python -m pip install --upgrade pip wheel setuptools

Diff for: .gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,5 @@ $RECYCLE.BIN/
166166

167167
audio/*
168168
keys/*
169+
keys*/
170+
env.sh

Diff for: README.md

+9
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,15 @@ python3 -m pip install -e '.[dev]'
4545
Before using, place API keys for the relevant services in the `/keys` folder
4646
(or a folder specified by the `ICESPEAK_KEYS_DIR` environment variable).
4747

48+
Alternately, you can set the following environment variables:
49+
50+
```sh
51+
export ICESPEAK_AWSPOLLY_API_KEY=your-aws-polly-api-key
52+
export ICESPEAK_AZURE_API_KEY=your-azure-api-key
53+
export ICESPEAK_GOOGLE_API_KEY=your-google-api-key
54+
export ICESPEAK_OPENAI_API_KEY=your-openai-api-key
55+
```
56+
4857
Output audio files are saved to the directory specified
4958
by the `ICESPEAK_AUDIO_DIR` environment variable.
5059
By default Icespeak creates the directory `<TEMP DIR>/icespeak`

Diff for: pyproject.toml

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
[project]
22
name = "icespeak"
3-
# Versioning is automatic (w/setuptools-scm)
4-
dynamic = ["version"]
3+
version = "0.3.7"
54
description = "Icespeak - Icelandic TTS library"
65
authors = [{ name = "Miðeind ehf.", email = "mideind@mideind.is" }]
76
readme = { file = "README.md", content-type = "text/markdown" }
@@ -26,13 +25,13 @@ classifiers = [
2625
requires-python = ">=3.9"
2726
dependencies = [
2827
# "aiohttp[speedups]>=3.8.4",
29-
"requests>=2.31.0",
30-
"typing-extensions>=4.7.1",
28+
"requests>=2.32.3",
29+
"typing-extensions>=4.12.2",
3130
"pydantic==2.3.0",
3231
"pydantic-settings>=2.0.3",
33-
"cachetools>=5.3.1",
32+
"cachetools>=5.5.0",
3433
# For parsing Icelandic text
35-
"islenska<1.0.0",
34+
"islenska<2.0.0",
3635
"reynir<4.0.0",
3736
"tokenizer<4.0.0",
3837
# Azure TTS
@@ -65,6 +64,7 @@ dev = [
6564
"ruff>=0.5.7",
6665
"pre-commit>=3.3.3",
6766
"mypy>=1.4.1",
67+
"boto3-stubs>=1.35.48",
6868
]
6969

7070
# *** Configuration of tools ***

Diff for: src/icespeak/settings.py

+69-40
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
# We dont import annotations from __future__ here
2525
# due to pydantic
2626
from typing import Any, Optional
27-
from typing_extensions import Literal
2827

2928
import json
3029
import os
@@ -114,10 +113,16 @@ class Settings(BaseSettings):
114113
"If not set, creates a directory in the platform's temporary directory."
115114
),
116115
)
117-
AUDIO_CACHE_SIZE: int = Field(default=300, gt=0, description="Max number of audio files to cache.")
118-
AUDIO_CACHE_CLEAN: bool = Field(default=True, description="If True, cleans up generated audio files upon exit.")
116+
AUDIO_CACHE_SIZE: int = Field(
117+
default=300, gt=-1, description="Max number of audio files to cache."
118+
)
119+
AUDIO_CACHE_CLEAN: bool = Field(
120+
default=True, description="If True, cleans up generated audio files upon exit."
121+
)
119122

120-
KEYS_DIR: Path = Field(default=Path("keys"), description="Where to look for API keys.")
123+
KEYS_DIR: Path = Field(
124+
default=Path("keys"), description="Where to look for API keys."
125+
)
121126
AWSPOLLY_KEY_FILENAME: str = Field(
122127
default="AWSPollyServerKey.json",
123128
description="Name of the AWS Polly API key file.",
@@ -182,26 +187,24 @@ class Keys(BaseModel):
182187

183188
azure: Optional[AzureKey] = Field(default=None, description="Azure API key.")
184189
aws: Optional[AWSPollyKey] = Field(default=None, description="AWS Polly API key.")
185-
google: Optional[dict[str, Any]] = Field(default=None, description="Google API key.")
186-
# TODO: Re-implement TTS with Tiro
187-
tiro: Literal[None] = Field(default=None)
190+
google: Optional[dict[str, Any]] = Field(
191+
default=None, description="Google API key."
192+
)
188193
openai: Optional[OpenAIKey] = Field(default=None, description="OpenAI API key.")
189194

190195
def __hash__(self):
191-
return hash((self.azure, self.aws, self.google, self.tiro, self.openai))
196+
return hash((self.azure, self.aws, self.google, self.openai))
192197

193198
def __eq__(self, other: object):
194199
return isinstance(other, Keys) and (
195200
self.azure,
196201
self.aws,
197202
self.google,
198-
self.tiro,
199203
self.openai,
200204
) == (
201205
other.azure,
202206
other.aws,
203207
other.google,
204-
other.tiro,
205208
other.openai,
206209
)
207210

@@ -210,36 +213,62 @@ def __eq__(self, other: object):
210213

211214
_kd = SETTINGS.KEYS_DIR
212215
if not (_kd.exists() and _kd.is_dir()):
213-
_LOG.warning("Keys directory missing or incorrect, TTS will not work! Set to: %s", _kd)
214-
else:
215-
# Load API keys, logging exceptions in level DEBUG so they aren't logged twice,
216-
# as exceptions are logged as warnings when voice modules are initialized
217-
try:
218-
API_KEYS.aws = AWSPollyKey.model_validate_json((_kd / SETTINGS.AWSPOLLY_KEY_FILENAME).read_text().strip())
219-
except Exception as err:
220-
_LOG.debug(
221-
"Could not load AWS Polly API key, ASR with AWS Polly will not work. Error: %s",
222-
err,
216+
_LOG.warning(
217+
"Keys directory missing or incorrect: %s", _kd
218+
)
219+
220+
# Load API keys, logging exceptions in level DEBUG so they aren't logged twice,
221+
# as exceptions are logged as warnings when voice modules are initialized
222+
223+
# Amazon Polly
224+
try:
225+
if key := os.getenv("ICESPEAK_AWSPOLLY_API_KEY"):
226+
API_KEYS.aws = AWSPollyKey.model_validate_json(key)
227+
else:
228+
API_KEYS.aws = AWSPollyKey.model_validate_json(
229+
(_kd / SETTINGS.AWSPOLLY_KEY_FILENAME).read_text().strip()
230+
)
231+
except Exception as err:
232+
_LOG.debug(
233+
"Could not load AWS Polly API key, ASR with AWS Polly will not work. Error: %s",
234+
err,
235+
)
236+
# Azure
237+
try:
238+
if key := os.getenv("ICESPEAK_AZURE_API_KEY"):
239+
API_KEYS.azure = AzureKey.model_validate_json(key)
240+
else:
241+
API_KEYS.azure = AzureKey.model_validate_json(
242+
(_kd / SETTINGS.AZURE_KEY_FILENAME).read_text().strip()
223243
)
224-
try:
225-
API_KEYS.azure = AzureKey.model_validate_json((_kd / SETTINGS.AZURE_KEY_FILENAME).read_text().strip())
226-
except Exception as err:
227-
_LOG.debug("Could not load Azure API key, ASR with Azure will not work. Error: %s", err)
228-
try:
229-
API_KEYS.google = json.loads((_kd / SETTINGS.GOOGLE_KEY_FILENAME).read_text().strip())
230-
except Exception as err:
231-
_LOG.debug(
232-
"Could not load Google API key, ASR with Google will not work. Error: %s",
233-
err,
244+
except Exception as err:
245+
_LOG.debug(
246+
"Could not load Azure API key, ASR with Azure will not work. Error: %s", err
247+
)
248+
# Google
249+
try:
250+
if key := os.getenv("ICESPEAK_GOOGLE_API_KEY"):
251+
API_KEYS.google = json.loads(key)
252+
else:
253+
API_KEYS.google = json.loads(
254+
(_kd / SETTINGS.GOOGLE_KEY_FILENAME).read_text().strip()
234255
)
235-
try:
236-
# First try to load the key from environment variable OPENAI_API_KEY
237-
if key := os.getenv("OPENAI_API_KEY"):
238-
API_KEYS.openai = OpenAIKey(api_key=SecretStr(key))
239-
else:
240-
API_KEYS.openai = OpenAIKey.model_validate_json((_kd / SETTINGS.OPENAI_KEY_FILENAME).read_text().strip())
241-
except Exception as err:
242-
_LOG.debug(
243-
"Could not load OpenAI API key, ASR with OpenAI will not work. Error: %s",
244-
err,
256+
except Exception as err:
257+
_LOG.debug(
258+
"Could not load Google API key, ASR with Google will not work. Error: %s",
259+
err,
260+
)
261+
# OpenAI
262+
try:
263+
# First try to load the key from environment variable OPENAI_API_KEY
264+
if key := os.getenv("ICESPEAK_OPENAI_API_KEY"):
265+
API_KEYS.openai = OpenAIKey(api_key=SecretStr(key))
266+
else:
267+
API_KEYS.openai = OpenAIKey.model_validate_json(
268+
(_kd / SETTINGS.OPENAI_KEY_FILENAME).read_text().strip()
245269
)
270+
except Exception as err:
271+
_LOG.debug(
272+
"Could not load OpenAI API key, ASR with OpenAI will not work. Error: %s",
273+
err,
274+
)

Diff for: src/icespeak/tts.py

+22-9
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,7 @@
3838
from .settings import SETTINGS, TRACE, Keys
3939
from .transcribe import TranscriptionOptions
4040

41-
# TODO: Re implement Tiro
42-
from .voices import BaseVoice, TTSOptions, VoiceInfoT, aws_polly, azure, openai, piper_tts # , google
41+
from .voices import BaseVoice, TTSOptions, VoiceInfoT, aws_polly, azure, openai, piper_tts
4342

4443
if TYPE_CHECKING:
4544
from pathlib import Path
@@ -71,7 +70,8 @@ def _setup_voices() -> tuple[VoicesT, ServicesT]:
7170
# Info about each voice
7271
if voice in voices:
7372
_LOG.warning(
74-
"Voice named %r already exists! " + "Skipping the one defined in module %s.",
73+
"Voice named %r already exists! "
74+
+ "Skipping the one defined in module %s.",
7575
voice,
7676
service.name,
7777
)
@@ -118,7 +118,9 @@ def _cleanup():
118118
audiofile.unlink(missing_ok=True)
119119

120120
# Small daemon thread which deletes files sent to the expired queue
121-
_cleanup_thread = threading.Thread(target=_cleanup, name="audio_cleanup", daemon=True)
121+
_cleanup_thread = threading.Thread(
122+
target=_cleanup, name="audio_cleanup", daemon=True
123+
)
122124
_cleanup_thread.start()
123125

124126
def _evict_all():
@@ -141,7 +143,7 @@ def _evict_all():
141143
atexit.register(_evict_all)
142144

143145

144-
@cached(_AUDIO_CACHE)
146+
# @cached(_AUDIO_CACHE)
145147
def tts_to_file(
146148
text: str,
147149
tts_options: TTSOptions | None = None,
@@ -164,11 +166,20 @@ def tts_to_file(
164166
"""
165167
if _LOG.isEnabledFor(DEBUG):
166168
_LOG.debug(
167-
"tts_to_file, text: %r, TTS options: %s, " + "transcribe: %r, transcription options: %s",
169+
"tts_to_file, text: %r, TTS options: %s, "
170+
+ "transcribe: %r, transcription options: %s",
168171
text,
169-
tts_options.model_dump(exclude_defaults=True) or "<default>" if tts_options else "None",
172+
(
173+
tts_options.model_dump(exclude_defaults=True) or "<default>"
174+
if tts_options
175+
else "None"
176+
),
170177
transcribe,
171-
transcription_options.model_dump(exclude_defaults=True) or "<default>" if transcription_options else "None",
178+
(
179+
transcription_options.model_dump(exclude_defaults=True) or "<default>"
180+
if transcription_options
181+
else "None"
182+
),
172183
)
173184
tts_options = tts_options or TTSOptions()
174185
try:
@@ -177,7 +188,9 @@ def tts_to_file(
177188
raise ValueError(f"Voice {tts_options.voice!r} not available.") from e
178189

179190
if tts_options.audio_format not in service.audio_formats:
180-
raise ValueError(f"Service {service.name} doesn't support audio format {tts_options.audio_format}.")
191+
raise ValueError(
192+
f"Service {service.name} doesn't support audio format {tts_options.audio_format}."
193+
)
181194

182195
if transcribe:
183196
transcription_options = transcription_options or TranscriptionOptions()

Diff for: tests/test_parser.py

+1-12
Original file line numberDiff line numberDiff line change
@@ -128,15 +128,4 @@ def test_greynirssmlparser():
128128
n = gp.transcribe(x)
129129
assert "&" not in n
130130
assert n.count("<") == 1
131-
assert n.count(">") == 1
132-
133-
# -------------------------
134-
# Test voice engine specific transcription
135-
136-
assert "Dora" in VOICES
137-
# Gudrun, the default voice, and Dora don't spell things the same
138-
gp2 = GreynirSSMLParser("Dora")
139-
alphabet = "aábcdðeéfghiíjklmnoópqrstuúvwxyýþæöz"
140-
n1 = gp.transcribe(gssml(alphabet, type="spell"))
141-
n2 = gp2.transcribe(gssml(alphabet, type="spell"))
142-
assert n1 != n2
131+
assert n.count(">") == 1

0 commit comments

Comments
 (0)