Skip to content

Commit 9bb60dc

Browse files
authored
feat/check-aws: feat: test and update aws connection within app (#11)
* feat/check-aws: feat: test and update aws connection within app * feat/check-aws: fix: address some comments
1 parent d2edbe3 commit 9bb60dc

File tree

10 files changed

+4390
-43
lines changed

10 files changed

+4390
-43
lines changed

Makefile

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
PYTHON = uv run python
2+
PIP = uv pip
3+
14
build:
2-
python -m build
5+
${PYTHON} -m build
36

47
clean:
58
find cracker -name "*.pyc" -exec rm {} +
@@ -8,20 +11,20 @@ clean:
811
rm -r build dist 2> /dev/null || true
912

1013
format-code:
11-
python -m isort cracker
12-
python -m black cracker
14+
${PYTHON} -m isort cracker
15+
${PYTHON} -m black cracker
1316

1417
upgrade:
15-
pip install --upgrade pip setuptools
18+
${PIP} install --upgrade pip setuptools
1619

1720
install: upgrade
18-
pip install -e .
21+
${PIP} install -e .
1922

2023
install-all: upgrade
21-
pip install -e .[dev,build]
24+
${PIP} install -e .[dev,build]
2225

2326
publish:
24-
python -m twine upload -r cracker dist/*
27+
${PYTHON} -m twine upload -r cracker dist/*
2528

2629
test:
27-
python -m pytest -v
30+
${PYTHON} -m pytest -v

cracker/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.8.1"
1+
__version__ = "0.9.0"

cracker/config/default.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ cracker:
66
speakers:
77
polly:
88
voice: Joanna
9-
profile_name: polly
9+
profile_name: default
1010

1111
espeak:
1212
voice: English

cracker/cracker.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ def __init__(self, app: QApplication):
3535
self.gui = MainWindow(self.config, speakers=self.SPEAKER)
3636
self.gui.speaker = self.speaker
3737
self.gui.player = self.player
38+
# Pass speaker reference to config window
39+
self.gui.config_window.speaker = self.speaker
3840

3941
self.key_manager = KeyBoardManager(self.app)
4042

cracker/cracker_gui.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def __init__(self, config: Configuration, speakers: SpeakersType):
4444
self.speaker: Optional[AbstractSpeaker] = None
4545
self.player: Optional[QMediaPlayer] = None
4646

47-
self.config_window = ConfigWindow()
47+
self.config_window = ConfigWindow(speaker=None)
4848

4949
def init(self):
5050
self.set_action()
@@ -224,6 +224,8 @@ def change_speaker(self, speaker_name: str):
224224
self.config.speaker = speaker_name
225225
self.config.load_speaker_config(speaker_name)
226226
self.init_values()
227+
# Update config window with new speaker reference
228+
self.config_window.speaker = self.speaker
227229

228230
def change_volume(self, volume):
229231
"""Volume should be on a percentage scale"""

cracker/speaker/polly.py

Lines changed: 145 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import logging
12
import os
2-
from typing import List
3+
from typing import List, Optional
34

45
import boto3
56
from PyQt5.QtCore import QUrl
67
from PyQt5.QtMultimedia import QMediaContent, QMediaPlaylist
8+
from PyQt5.QtWidgets import QMessageBox
79

810
from cracker.config import Configuration
911
from cracker.mp3_helper import create_filename, save_mp3
@@ -27,32 +29,116 @@ class Polly(AbstractSpeaker):
2729

2830
def __init__(self, player):
2931
self._cached_ssml = SSML()
30-
self._cached_filepath = ""
32+
self._cached_filepaths = []
3133
self._cached_voice = ""
3234

3335
self.config = Configuration()
34-
aws_profile = self.config.read_config()["polly"]["profile_name"]
35-
self._logger.debug("Using AWS profile: %s", aws_profile)
36-
self._connect_aws(aws_profile)
36+
self.client = None
37+
self._connection_error = None
38+
polly_config = self.config.read_config()["polly"]
39+
aws_profile = polly_config["profile_name"]
40+
aws_region = polly_config.get("region_name", None)
41+
self._logger.debug("Using AWS profile: %s, region: %s", aws_profile, aws_region)
42+
try:
43+
self.client = self._connect_aws(aws_profile, aws_region)
44+
except Exception as e:
45+
self._logger.error("Error connecting to AWS: %s", e)
46+
self._connection_error = str(e)
47+
3748
self.player = player
3849

3950
def __del__(self):
4051
try:
41-
os.remove(self._cached_filepath)
52+
for filepath in self._cached_filepaths:
53+
os.remove(filepath)
4254
except (OSError, TypeError):
4355
pass
4456

45-
def _connect_aws(self, profile_name: str):
57+
@staticmethod
58+
def _connect_aws(
59+
profile_name: Optional[str] = None, region_name: Optional[str] = None
60+
):
61+
"""Connect to AWS and create Polly client"""
4662
try:
47-
session = boto3.Session(profile_name=profile_name)
48-
self.client = session.client("polly")
63+
session = boto3.Session(profile_name=profile_name, region_name=region_name)
64+
return session.client("polly")
4965
except Exception as e:
50-
self._logger.exception(
51-
"Unable to connect to AWS with the profile '%s'. " "Please verify that configuration file exists.",
66+
logging.exception(
67+
"Unable to connect to AWS with the profile '%s' and region '%s'. "
68+
"Please verify that configuration file exists.",
5269
profile_name,
70+
region_name,
5371
)
5472
raise e
5573

74+
def reload_client(self):
75+
"""Reload the AWS Polly client with updated configuration"""
76+
self._logger.info("Reloading AWS Polly client with updated configuration")
77+
polly_config = self.config.read_config()["polly"]
78+
aws_profile = polly_config["profile_name"]
79+
aws_region = polly_config.get("region_name", None)
80+
81+
self._logger.debug(
82+
"Reloading with AWS profile: %s, region: %s", aws_profile, aws_region
83+
)
84+
85+
try:
86+
self.client = self._connect_aws(aws_profile, aws_region)
87+
self._connection_error = None # Clear any previous error
88+
self._logger.info("Successfully reloaded AWS Polly client")
89+
except Exception as e:
90+
self._logger.error("Error reloading AWS client: %s", e)
91+
self._connection_error = str(e)
92+
raise
93+
94+
@staticmethod
95+
def test_connection(
96+
profile_name: Optional[str] = None, region_name: Optional[str] = None
97+
):
98+
"""
99+
Test AWS Polly connection with given profile and region.
100+
101+
Args:
102+
profile_name: AWS profile name to use
103+
region_name: AWS region name to use (optional)
104+
105+
Returns:
106+
tuple: (success: bool, message: str)
107+
"""
108+
try:
109+
# Create session and client
110+
client = Polly._connect_aws(profile_name, region_name)
111+
112+
# Try to describe voices - this is a lightweight API call to verify connectivity
113+
response = client.describe_voices()
114+
115+
# Count available voices
116+
voice_count = len(response.get("Voices", []))
117+
118+
# Get the actual region being used
119+
actual_region = client.meta.region_name
120+
121+
success_message = (
122+
f"Profile: {profile_name or 'default'}\n"
123+
f"Region: {actual_region}\n"
124+
f"Available voices: {voice_count}"
125+
)
126+
127+
return True, success_message
128+
129+
except Exception as e:
130+
error_message = (
131+
f"Failed to connect to AWS Polly.\n\n"
132+
f"Profile: {profile_name or 'default'}\n"
133+
f"Region: {region_name or 'default'}\n\n"
134+
f"Error: {str(e)}\n\n"
135+
f"Please verify:\n"
136+
f"1. Your AWS profile is configured correctly\n"
137+
f"2. You are logged into AWS\n"
138+
f"3. Your credentials have access to AWS Polly"
139+
)
140+
return False, error_message
141+
56142
def save_cache(self, ssml: SSML, filepaths: List[str], voice):
57143
self._cached_ssml = ssml
58144
self._cached_filepaths = filepaths
@@ -75,24 +161,61 @@ def read_text(self, text: str, **config) -> None:
75161
self._logger.debug("Playing cached file")
76162
filepaths = self._cached_filepaths
77163
else:
78-
self._logger.debug("Re_cached_textquest from Polly")
164+
self._logger.debug("Request from Polly")
79165
filepaths = []
80166
# TODO: This should obviously be asynchronous!
81-
for idx, parted_text in enumerate(split_text):
82-
parted_ssml = SSML(parted_text, rate=rate, volume=volume)
83-
response = self.ask_polly(str(parted_ssml), voice)
84-
filename = create_filename(AbstractSpeaker.TMP_FILEPATH, idx)
85-
saved_filepath = save_mp3(response["AudioStream"].read(), filename)
86-
filepaths.append(saved_filepath)
87-
self.save_cache(ssml, filepaths, voice)
167+
try:
168+
for idx, parted_text in enumerate(split_text):
169+
parted_ssml = SSML(parted_text, rate=rate, volume=volume)
170+
response = self.ask_polly(str(parted_ssml), voice)
171+
filename = create_filename(AbstractSpeaker.TMP_FILEPATH, idx)
172+
saved_filepath = save_mp3(response["AudioStream"].read(), filename)
173+
filepaths.append(saved_filepath)
174+
self.save_cache(ssml, filepaths, voice)
175+
except (RuntimeError, Exception) as e:
176+
self._logger.error("Failed to read text with Polly: %s", e)
177+
return # Exit gracefully without crashing
88178
self.play_files(filepaths)
89179
return
90180

181+
def _show_error_dialog(self, message: str, details: str = ""):
182+
"""Shows an error dialog to the user"""
183+
msg_box = QMessageBox()
184+
msg_box.setIcon(QMessageBox.Critical)
185+
msg_box.setWindowTitle("AWS Polly Connection Error")
186+
msg_box.setText(message)
187+
if details:
188+
msg_box.setInformativeText(details)
189+
msg_box.setStandardButtons(QMessageBox.Ok)
190+
msg_box.exec_()
191+
91192
def ask_polly(self, ssml_text: str, voice: str):
92193
"""Connects to Polly and returns path to save mp3"""
93-
speech = self.create_speech(ssml_text, voice)
94-
response = self.client.synthesize_speech(**speech)
95-
return response
194+
if self.client is None or self._connection_error:
195+
error_msg = (
196+
"Unable to connect to AWS Polly. Please check your AWS configuration.\n\n"
197+
"Please verify:\n"
198+
"1. Your AWS profile is configured correctly\n"
199+
"2. You are logged into AWS\n"
200+
"3. Your credentials have access to AWS Polly"
201+
)
202+
self._logger.error("Attempted to use Polly without valid AWS connection")
203+
self._show_error_dialog(
204+
error_msg, f"Error details: {self._connection_error}"
205+
)
206+
raise RuntimeError("AWS Polly client not initialized")
207+
208+
try:
209+
speech = self.create_speech(ssml_text, voice)
210+
response = self.client.synthesize_speech(**speech)
211+
return response
212+
except Exception as e:
213+
error_msg = (
214+
"An error occurred while trying to synthesize speech with AWS Polly."
215+
)
216+
self._logger.error("Error calling Polly synthesize_speech: %s", e)
217+
self._show_error_dialog(error_msg, f"Error details: {str(e)}")
218+
raise
96219

97220
@staticmethod
98221
def create_speech(ssml_text: str, voice: str):

cracker/view/config_window.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@
1010
class ConfigWindow(QWidget):
1111
_logger = logging.getLogger(__name__)
1212

13-
def __init__(self):
13+
def __init__(self, speaker=None):
1414
super().__init__()
1515

1616
self.config = Configuration()
17+
self.speaker = speaker
1718
self.setWindowTitle("Configuration")
1819

1920
self.tabs = QTabWidget()
@@ -52,6 +53,15 @@ def confirm_action(self):
5253
self._logger.debug("Confirm action")
5354
self.config.regex_config = self.parser_tab.confirm_action()
5455
self.speakers_tab.confirm_action()
56+
57+
# Reload Polly client if speaker is Polly and it has a reload method
58+
if self.speaker and hasattr(self.speaker, "reload_client"):
59+
try:
60+
self._logger.debug("Reloading Polly client with updated configuration")
61+
self.speaker.reload_client()
62+
except Exception as e:
63+
self._logger.error("Failed to reload Polly client: %s", e)
64+
5565
self.hide()
5666

5767
def clearLayout(layout):

cracker/view/speaker_config_tab.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import logging
22

3-
from PyQt5.QtWidgets import QGridLayout, QLabel, QLineEdit, QWidget
3+
from PyQt5.QtWidgets import QGridLayout, QLabel, QLineEdit, QMessageBox, QPushButton, QWidget
44

55
from cracker.config import Configuration
66

@@ -39,6 +39,50 @@ def __init__(self):
3939
self._layout.addWidget(self.aws_region_label, 2, 0)
4040
self._layout.addWidget(self.aws_region_input, 2, 1)
4141

42+
# Test connection button
43+
self.test_connection_btn = QPushButton("Test Connection")
44+
self.test_connection_btn.released.connect(self.test_connection)
45+
self._layout.addWidget(self.test_connection_btn, 3, 0, 1, 2)
46+
47+
def test_connection(self):
48+
"""Test AWS Polly connection with the entered profile and region"""
49+
self._logger.info("Testing AWS Polly connection")
50+
51+
profile_name = self.aws_profile_input.text() or "default"
52+
region_name = self.aws_region_input.text() or None
53+
54+
from cracker.speaker.polly import Polly
55+
56+
try:
57+
# Test the connection
58+
success, message = Polly.test_connection(profile_name, region_name)
59+
60+
# Show result dialog
61+
msg_box = QMessageBox()
62+
if success:
63+
msg_box.setIcon(QMessageBox.Information)
64+
msg_box.setWindowTitle("Connection Successful")
65+
msg_box.setText("Successfully connected to AWS Polly!")
66+
msg_box.setInformativeText(message)
67+
else:
68+
msg_box.setIcon(QMessageBox.Warning)
69+
msg_box.setWindowTitle("Connection Failed")
70+
msg_box.setText("Failed to connect to AWS Polly")
71+
msg_box.setInformativeText(message)
72+
73+
msg_box.setStandardButtons(QMessageBox.Ok)
74+
msg_box.exec_()
75+
76+
except Exception as e:
77+
self._logger.error("Error testing connection: %s", e)
78+
msg_box = QMessageBox()
79+
msg_box.setIcon(QMessageBox.Critical)
80+
msg_box.setWindowTitle("Connection Test Error")
81+
msg_box.setText("An unexpected error occurred while testing the connection")
82+
msg_box.setInformativeText(str(e))
83+
msg_box.setStandardButtons(QMessageBox.Ok)
84+
msg_box.exec_()
85+
4286
def confirm_action(self):
4387
self._logger.info("Confirming action")
4488

0 commit comments

Comments
 (0)