1+ import logging
12import os
2- from typing import List
3+ from typing import List , Optional
34
45import boto3
56from PyQt5 .QtCore import QUrl
67from PyQt5 .QtMultimedia import QMediaContent , QMediaPlaylist
8+ from PyQt5 .QtWidgets import QMessageBox
79
810from cracker .config import Configuration
911from 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 ):
0 commit comments