@@ -27,18 +27,42 @@ def __init__(
2727 poll_interval : float = 2.0 ,
2828 submit_timeout : float = 1800.0 ,
2929 poll_timeout : float = 30.0 ,
30+ download_connect_timeout : float = 20.0 ,
31+ download_read_timeout : float = 300.0 ,
32+ download_max_retries : int = 3 ,
33+ download_retry_delay : float = 2.0 ,
3034 ):
3135 if not access_token :
3236 raise ValueError ("access_token is required and cannot be None or empty" )
3337 if not base_url :
3438 raise ValueError ("base_url is required and cannot be None or empty" )
39+ if max_retries < 1 :
40+ raise ValueError ("max_retries must be at least 1" )
41+ if poll_interval <= 0 :
42+ raise ValueError ("poll_interval must be > 0" )
43+ if submit_timeout <= 0 :
44+ raise ValueError ("submit_timeout must be > 0" )
45+ if poll_timeout <= 0 :
46+ raise ValueError ("poll_timeout must be > 0" )
47+ if download_connect_timeout <= 0 :
48+ raise ValueError ("download_connect_timeout must be > 0" )
49+ if download_read_timeout <= 0 :
50+ raise ValueError ("download_read_timeout must be > 0" )
51+ if download_max_retries < 1 :
52+ raise ValueError ("download_max_retries must be at least 1" )
53+ if download_retry_delay < 0 :
54+ raise ValueError ("download_retry_delay must be >= 0" )
3555
3656 self .access_token = access_token
3757 self .base_url = base_url .rstrip ("/" )
3858 self .max_retries = max_retries
3959 self .poll_interval = poll_interval
4060 self .submit_timeout = submit_timeout
4161 self .poll_timeout = poll_timeout
62+ self .download_connect_timeout = download_connect_timeout
63+ self .download_read_timeout = download_read_timeout
64+ self .download_max_retries = download_max_retries
65+ self .download_retry_delay = download_retry_delay
4266
4367 self ._setup ()
4468
@@ -188,25 +212,66 @@ def _poll_tts_result(self, task_id: str) -> str:
188212
189213 def _download_audio (self , audio_url : str , output_path : Path ) -> None :
190214 """Download audio file from URL to output_path."""
191- try :
192- response = requests .get ( audio_url , timeout = 300.0 , stream = True )
193- response . raise_for_status ()
215+ temp_output_path = output_path . with_suffix ( f" { output_path . suffix } .part" )
216+ last_timeout_error : requests .exceptions . Timeout | None = None
217+ last_network_error : requests . exceptions . RequestException | None = None
194218
195- # Write to file in chunks
196- with open (output_path , "wb" ) as f :
197- for chunk in response .iter_content (chunk_size = 8192 ):
198- if chunk :
199- f .write (chunk )
219+ for attempt in range (1 , self .download_max_retries + 1 ):
220+ try :
221+ with requests .get (
222+ audio_url ,
223+ timeout = (self .download_connect_timeout , self .download_read_timeout ),
224+ stream = True ,
225+ ) as response :
226+ response .raise_for_status ()
200227
201- except requests .exceptions .Timeout as e :
202- raise TimeoutError ("Download timeout after 300 seconds" ) from e
203- except requests .exceptions .HTTPError as e :
204- # HTTPError always has a response attribute
205- resp = e .response
206- if resp is not None :
207- raise RuntimeError (f"Download failed with HTTP Error { resp .status_code } : { resp .text } " ) from e
208- raise RuntimeError (f"Download failed with HTTP Error: { e } " ) from e
209- except requests .exceptions .RequestException as e :
210- raise ConnectionError (f"Download failed: { e } " ) from e
211- except OSError as e :
212- raise RuntimeError (f"Failed to write audio file to { output_path } : { e } " ) from e
228+ with open (temp_output_path , "wb" ) as f :
229+ for chunk in response .iter_content (chunk_size = 8192 ):
230+ if chunk :
231+ f .write (chunk )
232+
233+ temp_output_path .replace (output_path )
234+ return
235+
236+ except requests .exceptions .Timeout as e :
237+ temp_output_path .unlink (missing_ok = True )
238+ last_timeout_error = e
239+ if attempt < self .download_max_retries :
240+ time .sleep (self .download_retry_delay * attempt )
241+ continue
242+
243+ except requests .exceptions .HTTPError as e :
244+ temp_output_path .unlink (missing_ok = True )
245+ resp = e .response
246+ status_code = resp .status_code if resp is not None else None
247+ retryable_status_codes = {408 , 425 , 429 , 500 , 502 , 503 , 504 }
248+ if status_code in retryable_status_codes and attempt < self .download_max_retries :
249+ time .sleep (self .download_retry_delay * attempt )
250+ continue
251+
252+ if resp is not None :
253+ raise RuntimeError (f"Download failed with HTTP Error { resp .status_code } : { resp .text } " ) from e
254+ raise RuntimeError (f"Download failed with HTTP Error: { e } " ) from e
255+
256+ except requests .exceptions .RequestException as e :
257+ temp_output_path .unlink (missing_ok = True )
258+ last_network_error = e
259+ if attempt < self .download_max_retries :
260+ time .sleep (self .download_retry_delay * attempt )
261+ continue
262+ raise ConnectionError (f"Download failed after { self .download_max_retries } attempts: { e } " ) from e
263+
264+ except OSError as e :
265+ temp_output_path .unlink (missing_ok = True )
266+ raise RuntimeError (f"Failed to write audio file to { output_path } : { e } " ) from e
267+
268+ if last_timeout_error is not None :
269+ raise TimeoutError (
270+ f"Download timeout after { self .download_max_retries } attempts "
271+ f"(connect_timeout={ self .download_connect_timeout } s, read_timeout={ self .download_read_timeout } s)"
272+ ) from last_timeout_error
273+
274+ if last_network_error is not None :
275+ raise ConnectionError (f"Download failed after { self .download_max_retries } attempts" ) from last_network_error
276+
277+ raise RuntimeError ("Download failed due to an unknown error" )
0 commit comments