Skip to content

Commit ac37686

Browse files
committed
feat: VOICEVOXバックアップサーバーを指定できるように。
1 parent 74cd730 commit ac37686

2 files changed

Lines changed: 138 additions & 4 deletions

File tree

.env.example

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@ DB_SSL=false
1010
DISCORD_TOKEN=your_discord_token
1111

1212
# 再接続設定
13+
# true: 有効, false: 無効
14+
# 起動時にボイスチャンネルへの再接続を試みるかどうか
1315
reconnect=false
1416

1517
# シャード数
18+
# 1以上の整数を指定
1619
SHARD_COUNT=1
1720

1821

@@ -29,5 +32,12 @@ SENTRY_DSN=your_sentry_dsn
2932
# 複数指定するとランダムにサーバーを使用して負荷分散するようになる
3033
VOICEVOX_URL=http://voicevoxserverurl:port
3134

35+
# VOICEVOXバックアップサーバーURL(オプション)
36+
# 上記のVOICEVOX_URLが利用できない場合に使用される。
37+
# 指定しない場合、通常エラーを返す。
38+
#
39+
# VOICEVOX_BACKUP_URL=http://voicevoxxbackupserverurl:port
40+
3241
# 管理者ID(DiscordユーザーID)
42+
# 管理者コマンドが使用可能になる
3343
ADMIN_ID=your_discord_user_id

lib/VOICEVOXlib.py

Lines changed: 128 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
from prometheus_client import Gauge
99
import random
1010
import logging # 追加: エラーログ用
11+
try:
12+
import sentry_sdk
13+
except ImportError:
14+
sentry_sdk = None
1115

1216
# Load environment variables
1317
load_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

Comments
 (0)