88from prometheus_client import Gauge
99import random
1010import logging # 追加: エラーログ用
11+ try :
12+ import sentry_sdk
13+ except ImportError :
14+ sentry_sdk = None
1115
1216# Load environment variables
1317load_dotenv ()
@@ -24,6 +28,7 @@ def __init__(self, base_url=None):
2428 self ._default_url = "http://localhost:50021"
2529 # 初期化時は一度だけロード
2630 self .base_urls = self ._load_base_urls ()
31+ self .backup_urls = self ._load_backup_urls ()
2732 # プロジェクトルートの tmp ディレクトリを確保
2833 # lib ディレクトリの親をプロジェクトルートとみなし、その直下に tmp を作成する
2934 try :
@@ -54,6 +59,11 @@ def _load_base_urls(self):
5459 else :
5560 return [self ._base_url_arg ]
5661
62+ def _load_backup_urls (self ):
63+ load_dotenv (override = True )
64+ backup_env_urls = os .getenv ("VOICEVOX_BACKUP_URL" , "" )
65+ return [u .strip () for u in backup_env_urls .split ("," ) if u .strip ()]
66+
5767 def _choose_base_url (self ):
5868 # .envを毎回再読込してURLリストを更新
5969 self .base_urls = self ._load_base_urls ()
@@ -79,10 +89,12 @@ async def synthesize(self, text, speaker_id, output_path, speed: float = 1.0):
7989 """
8090 # .envを毎回再読込してURLリストを更新
8191 self .base_urls = self ._load_base_urls ()
92+ self .backup_urls = self ._load_backup_urls ()
93+ last_error = None
94+ # まず通常サーバーで試行
8295 for base_url in self .base_urls :
8396 if os .getenv ("DEBUG" ) == "1" :
8497 print (f"Using VOICEVOX URL: { base_url } " )
85- last_error = None
8698 for attempt in range (3 ):
8799 try :
88100 async with aiohttp .ClientSession () as session :
@@ -166,8 +178,73 @@ async def synthesize(self, text, speaker_id, output_path, speed: float = 1.0):
166178 continue
167179 except Exception as e :
168180 logging .error (f"VOICEVOX synthesis unexpected error for URL { base_url } : { e } " )
181+ last_error = e
169182 break
170- # 3回とも失敗した場合のみ次のURLへ
183+ # 通常サーバー全て失敗→バックアップサーバーで再試行
184+ if self .backup_urls :
185+ for backup_url in self .backup_urls :
186+ try :
187+ async with aiohttp .ClientSession () as session :
188+ start_time = time .perf_counter ()
189+ # Step 1: Generate audio query
190+ async with session .post (
191+ f"{ backup_url } /audio_query" ,
192+ params = {"text" : text , "speaker" : speaker_id }
193+ ) as query_response :
194+ query_response .raise_for_status ()
195+ audio_query = await query_response .json ()
196+ if "speedScale" in audio_query :
197+ audio_query ["speedScale" ] = speed
198+
199+ # Step 2: Synthesize audio
200+ async with session .post (
201+ f"{ backup_url } /synthesis" ,
202+ params = {"speaker" : speaker_id },
203+ json = audio_query
204+ ) as synthesis_response :
205+ synthesis_response .raise_for_status ()
206+ wav_bytes = await synthesis_response .read ()
207+
208+ elapsed = time .perf_counter () - start_time
209+
210+ # Step 3: Save WAV file and compute duration
211+ with wave .open (io .BytesIO (wav_bytes ), "rb" ) as wav_file :
212+ n_frames = wav_file .getnframes ()
213+ framerate = wav_file .getframerate ()
214+ duration_sec = n_frames / framerate if framerate else 0.0
215+
216+ # Update Prometheus metric: seconds of processing per 1 minute of audio
217+ if duration_sec > 0 :
218+ seconds_per_minute = elapsed * 60.0 / duration_sec
219+ else :
220+ seconds_per_minute = 0.0
221+ try :
222+ VOICE_GENERATION_TIME_PER_MINUTE .set (seconds_per_minute )
223+ except Exception :
224+ pass
225+
226+ filename = os .path .basename (output_path )
227+ if self .tmp_dir :
228+ tmp_output_path = os .path .join (self .tmp_dir , filename )
229+ else :
230+ tmp_output_path = os .path .abspath (output_path )
231+
232+ # tmp に保存
233+ with wave .open (tmp_output_path , "wb" ) as output_file :
234+ output_file .setparams (wav_file .getparams ())
235+ output_file .writeframes (wav_file .readframes (n_frames ))
236+
237+ # SentryにINFOログ送信
238+ if sentry_sdk :
239+ sentry_sdk .capture_message (
240+ f"VOICEVOX backup server used: { backup_url } for text: { text [:50 ]} ..." ,
241+ level = "info"
242+ )
243+ return tmp_output_path
244+ except Exception as e :
245+ logging .error (f"VOICEVOX backup synthesis failed for URL { backup_url } : { e } " )
246+ last_error = e
247+ continue
171248 raise RuntimeError (f"All VOICEVOX URLs failed for synthesis: { text [:50 ]} ... Last error: { last_error } " )
172249
173250 async def synthesize_bytes (self , text , speaker_id ) -> tuple [str , bytes ]:
@@ -183,10 +260,11 @@ async def synthesize_bytes(self, text, speaker_id) -> tuple[str, bytes]:
183260 """
184261 # .envを毎回再読込してURLリストを更新
185262 self .base_urls = self ._load_base_urls ()
263+ self .backup_urls = self ._load_backup_urls ()
264+ last_error = None
186265 for base_url in self .base_urls :
187266 if os .getenv ("DEBUG" ) == "1" :
188267 print (f"Using VOICEVOX URL: { base_url } " )
189- last_error = None
190268 for attempt in range (3 ):
191269 try :
192270 async with aiohttp .ClientSession () as session :
@@ -255,8 +333,54 @@ async def synthesize_bytes(self, text, speaker_id) -> tuple[str, bytes]:
255333 continue
256334 except Exception as e :
257335 logging .error (f"VOICEVOX synthesize_bytes unexpected error for URL { base_url } : { e } " )
336+ last_error = e
258337 break
259- # 3回とも失敗した場合のみ次のURLへ
338+ # 通常サーバー全て失敗→バックアップサーバーで再試行
339+ if self .backup_urls :
340+ for backup_url in self .backup_urls :
341+ try :
342+ async with aiohttp .ClientSession () as session :
343+ start_time = time .perf_counter ()
344+ # Step 1: Generate audio query
345+ async with session .post (
346+ f"{ backup_url } /audio_query" ,
347+ params = {"text" : text , "speaker" : speaker_id }
348+ ) as query_response :
349+ query_response .raise_for_status ()
350+ audio_query = await query_response .json ()
351+
352+ # Step 2: Synthesize audio
353+ async with session .post (
354+ f"{ backup_url } /synthesis" ,
355+ params = {"speaker" : speaker_id },
356+ json = audio_query
357+ ) as synthesis_response :
358+ synthesis_response .raise_for_status ()
359+ wav_bytes = await synthesis_response .read ()
360+
361+ try :
362+ with wave .open (io .BytesIO (wav_bytes ), "rb" ) as wav_file :
363+ n_frames = wav_file .getnframes ()
364+ framerate = wav_file .getframerate ()
365+ duration_sec = n_frames / framerate if framerate else 0.0
366+ if duration_sec > 0 :
367+ seconds_per_minute = elapsed * 60.0 / duration_sec
368+ else :
369+ seconds_per_minute = 0.0
370+ VOICE_GENERATION_TIME_PER_MINUTE .set (seconds_per_minute )
371+ except Exception :
372+ pass
373+ # SentryにINFOログ送信
374+ if sentry_sdk :
375+ sentry_sdk .capture_message (
376+ f"VOICEVOX backup server used: { backup_url } for text: { text [:50 ]} ..." ,
377+ level = "info"
378+ )
379+ return backup_url , wav_bytes
380+ except Exception as e :
381+ logging .error (f"VOICEVOX backup synthesize_bytes failed for URL { backup_url } : { e } " )
382+ last_error = e
383+ continue
260384 raise RuntimeError (f"All VOICEVOX URLs failed for synthesize_bytes: { text [:50 ]} ... Last error: { last_error } " )
261385
262386# Example usage:
0 commit comments