Skip to content

Commit 74cd730

Browse files
authored
Merge pull request techfish-11#34 from SwiftlyBots/main
VOICEVOX合成のリトライ機能追加と、Prometheusメトリクスの強化
2 parents b07e9fb + 431d7c2 commit 74cd730

2 files changed

Lines changed: 179 additions & 108 deletions

File tree

cogs/system/prometheus.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import discord
22
from discord.ext import commands, tasks
33
from prometheus_client import Gauge, start_http_server
4+
from collections import defaultdict
45

56
class PrometheusCog(commands.Cog):
67
def __init__(self, bot):
@@ -10,12 +11,19 @@ def __init__(self, bot):
1011
self.latency_metric = Gauge('bot_latency_ms', 'botのレイテンシ(ms)')
1112
self.tts_count_per_minute = Gauge('bot_tts_count_per_minute', '1分間に読み上げた回数')
1213
self.error_count_per_minute = Gauge('bot_error_count_per_minute', '1分間に起きたエラー数')
14+
self.command_counters = defaultdict(int)
15+
self.command_gauges = {}
1316
self.update_metrics.start()
1417
start_http_server(47724)
1518

1619
def cog_unload(self):
1720
self.update_metrics.cancel()
1821

22+
@commands.Cog.listener()
23+
async def on_application_command(self, ctx):
24+
cmd_name = ctx.command.name if hasattr(ctx, "command") else "unknown"
25+
self.command_counters[cmd_name] += 1
26+
1927
@tasks.loop(minutes=1)
2028
async def update_metrics(self):
2129
vc_count = len(self.bot.voice_clients) if self.bot.voice_clients else 0
@@ -26,6 +34,15 @@ async def update_metrics(self):
2634
self.latency_metric.set(latency_ms)
2735
self.tts_count_per_minute.set(self.bot.tts_counter)
2836
self.error_count_per_minute.set(self.bot.error_counter)
37+
# 各コマンドの1分間使用回数をPrometheus Gaugeにセット
38+
for cmd_name, count in self.command_counters.items():
39+
if cmd_name not in self.command_gauges:
40+
self.command_gauges[cmd_name] = Gauge(
41+
f'bot_command_{cmd_name}_count_per_minute',
42+
f'1分間に使用されたコマンド {cmd_name} の回数'
43+
)
44+
self.command_gauges[cmd_name].set(count)
45+
self.command_counters.clear()
2946
# カウンターをリセット
3047
self.bot.tts_counter = 0
3148
self.bot.error_counter = 0

lib/VOICEVOXlib.py

Lines changed: 162 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -79,70 +79,96 @@ async def synthesize(self, text, speaker_id, output_path, speed: float = 1.0):
7979
"""
8080
# .envを毎回再読込してURLリストを更新
8181
self.base_urls = self._load_base_urls()
82-
for base_url in self.base_urls: # 各URLを順番に試行
82+
for base_url in self.base_urls:
8383
if os.getenv("DEBUG") == "1":
84-
print(f"Using VOICEVOX URL: {base_url}") # 追加: 使用するURLをprint
85-
try:
86-
async with aiohttp.ClientSession() as session:
87-
start_time = time.perf_counter() # 計測開始
88-
89-
# Step 1: Generate audio query
90-
async with session.post(
91-
f"{base_url}/audio_query",
92-
params={"text": text, "speaker": speaker_id}
93-
) as query_response:
94-
query_response.raise_for_status()
95-
audio_query = await query_response.json()
96-
# スピードを上書き
97-
if "speedScale" in audio_query:
98-
audio_query["speedScale"] = speed
99-
100-
# Step 2: Synthesize audio
101-
async with session.post(
102-
f"{base_url}/synthesis",
103-
params={"speaker": speaker_id},
104-
json=audio_query
105-
) as synthesis_response:
106-
synthesis_response.raise_for_status()
107-
wav_bytes = await synthesis_response.read()
108-
109-
elapsed = time.perf_counter() - start_time # 秒
110-
111-
# Step 3: Save WAV file and compute duration
112-
with wave.open(io.BytesIO(wav_bytes), "rb") as wav_file:
113-
n_frames = wav_file.getnframes()
114-
framerate = wav_file.getframerate()
115-
duration_sec = n_frames / framerate if framerate else 0.0
116-
117-
# Update Prometheus metric: seconds of processing per 1 minute of audio
118-
if duration_sec > 0:
119-
seconds_per_minute = elapsed * 60.0 / duration_sec
120-
else:
121-
seconds_per_minute = 0.0
122-
try:
123-
VOICE_GENERATION_TIME_PER_MINUTE.set(seconds_per_minute)
124-
except Exception:
125-
# 安全のため例外は無視(メトリクス失敗で処理を止めない)
126-
pass
84+
print(f"Using VOICEVOX URL: {base_url}")
85+
last_error = None
86+
for attempt in range(3):
87+
try:
88+
async with aiohttp.ClientSession() as session:
89+
start_time = time.perf_counter()
90+
# Step 1: Generate audio query
91+
async with session.post(
92+
f"{base_url}/audio_query",
93+
params={"text": text, "speaker": speaker_id}
94+
) as query_response:
95+
if query_response.status >= 500:
96+
raise aiohttp.ClientResponseError(
97+
request_info=query_response.request_info,
98+
history=query_response.history,
99+
status=query_response.status,
100+
message=f"HTTP {query_response.status}",
101+
headers=query_response.headers
102+
)
103+
query_response.raise_for_status()
104+
audio_query = await query_response.json()
105+
if "speedScale" in audio_query:
106+
audio_query["speedScale"] = speed
107+
108+
# Step 2: Synthesize audio
109+
async with session.post(
110+
f"{base_url}/synthesis",
111+
params={"speaker": speaker_id},
112+
json=audio_query
113+
) as synthesis_response:
114+
if synthesis_response.status >= 500:
115+
raise aiohttp.ClientResponseError(
116+
request_info=synthesis_response.request_info,
117+
history=synthesis_response.history,
118+
status=synthesis_response.status,
119+
message=f"HTTP {synthesis_response.status}",
120+
headers=synthesis_response.headers
121+
)
122+
synthesis_response.raise_for_status()
123+
wav_bytes = await synthesis_response.read()
124+
125+
elapsed = time.perf_counter() - start_time
126+
127+
# Step 3: Save WAV file and compute duration
128+
with wave.open(io.BytesIO(wav_bytes), "rb") as wav_file:
129+
n_frames = wav_file.getnframes()
130+
framerate = wav_file.getframerate()
131+
duration_sec = n_frames / framerate if framerate else 0.0
132+
133+
# Update Prometheus metric: seconds of processing per 1 minute of audio
134+
if duration_sec > 0:
135+
seconds_per_minute = elapsed * 60.0 / duration_sec
136+
else:
137+
seconds_per_minute = 0.0
138+
try:
139+
VOICE_GENERATION_TIME_PER_MINUTE.set(seconds_per_minute)
140+
except Exception:
141+
# 安全のため例外は無視(メトリクス失敗で処理を止めない)
142+
pass
143+
144+
# 出力先をプロジェクトルートの tmp ディレクトリに固定し、そのパスを返す
145+
filename = os.path.basename(output_path)
146+
if self.tmp_dir:
147+
tmp_output_path = os.path.join(self.tmp_dir, filename)
148+
else:
149+
tmp_output_path = os.path.abspath(output_path)
127150

128-
# 出力先をプロジェクトルートの tmp ディレクトリに固定し、そのパスを返す
129-
filename = os.path.basename(output_path)
130-
if self.tmp_dir:
131-
tmp_output_path = os.path.join(self.tmp_dir, filename)
132-
else:
133-
tmp_output_path = os.path.abspath(output_path)
134-
135-
# tmp に保存
136-
with wave.open(tmp_output_path, "wb") as output_file:
137-
output_file.setparams(wav_file.getparams())
138-
output_file.writeframes(wav_file.readframes(n_frames))
139-
140-
return tmp_output_path
141-
except aiohttp.ClientError as e:
142-
logging.error(f"VOICEVOX synthesis failed for URL {base_url}: {e}")
143-
continue # 次のURLを試行
144-
# すべてのURLで失敗した場合
145-
raise RuntimeError(f"All VOICEVOX URLs failed for synthesis: {text[:50]}...")
151+
# tmp に保存
152+
with wave.open(tmp_output_path, "wb") as output_file:
153+
output_file.setparams(wav_file.getparams())
154+
output_file.writeframes(wav_file.readframes(n_frames))
155+
156+
return tmp_output_path
157+
except aiohttp.ClientResponseError as e:
158+
logging.error(f"VOICEVOX synthesis failed for URL {base_url} (attempt {attempt+1}/3): {e}")
159+
last_error = e
160+
time.sleep(0.5)
161+
continue
162+
except aiohttp.ClientError as e:
163+
logging.error(f"VOICEVOX synthesis failed for URL {base_url} (attempt {attempt+1}/3): {e}")
164+
last_error = e
165+
time.sleep(0.5)
166+
continue
167+
except Exception as e:
168+
logging.error(f"VOICEVOX synthesis unexpected error for URL {base_url}: {e}")
169+
break
170+
# 3回とも失敗した場合のみ次のURLへ
171+
raise RuntimeError(f"All VOICEVOX URLs failed for synthesis: {text[:50]}... Last error: {last_error}")
146172

147173
async def synthesize_bytes(self, text, speaker_id) -> tuple[str, bytes]:
148174
"""
@@ -157,53 +183,81 @@ async def synthesize_bytes(self, text, speaker_id) -> tuple[str, bytes]:
157183
"""
158184
# .envを毎回再読込してURLリストを更新
159185
self.base_urls = self._load_base_urls()
160-
for base_url in self.base_urls: # 各URLを順番に試行
186+
for base_url in self.base_urls:
161187
if os.getenv("DEBUG") == "1":
162-
print(f"Using VOICEVOX URL: {base_url}") # 追加: 使用するURLをprint
163-
try:
164-
async with aiohttp.ClientSession() as session:
165-
start_time = time.perf_counter() # 計測開始
166-
167-
# Step 1: Generate audio query
168-
async with session.post(
169-
f"{base_url}/audio_query",
170-
params={"text": text, "speaker": speaker_id}
171-
) as query_response:
172-
query_response.raise_for_status()
173-
audio_query = await query_response.json()
174-
175-
# Step 2: Synthesize audio
176-
async with session.post(
177-
f"{base_url}/synthesis",
178-
params={"speaker": speaker_id},
179-
json=audio_query
180-
) as synthesis_response:
181-
synthesis_response.raise_for_status()
182-
wav_bytes = await synthesis_response.read()
183-
184-
elapsed = time.perf_counter() - start_time # 秒
185-
186-
# Compute duration and set metric
187-
try:
188-
with wave.open(io.BytesIO(wav_bytes), "rb") as wav_file:
189-
n_frames = wav_file.getnframes()
190-
framerate = wav_file.getframerate()
191-
duration_sec = n_frames / framerate if framerate else 0.0
192-
if duration_sec > 0:
193-
seconds_per_minute = elapsed * 60.0 / duration_sec
194-
else:
195-
seconds_per_minute = 0.0
196-
VOICE_GENERATION_TIME_PER_MINUTE.set(seconds_per_minute)
197-
except Exception:
198-
# 例外は無視して wav_bytes を返す(メトリクスの失敗で処理を止めない)
199-
pass
200-
201-
return base_url, wav_bytes # 変更: URL とバイトデータをタプルで返す
202-
except aiohttp.ClientError as e:
203-
logging.error(f"VOICEVOX synthesize_bytes failed for URL {base_url}: {e}")
204-
continue # 次のURLを試行
205-
# すべてのURLで失敗した場合
206-
raise RuntimeError(f"All VOICEVOX URLs failed for synthesize_bytes: {text[:50]}...")
188+
print(f"Using VOICEVOX URL: {base_url}")
189+
last_error = None
190+
for attempt in range(3):
191+
try:
192+
async with aiohttp.ClientSession() as session:
193+
start_time = time.perf_counter()
194+
195+
# Step 1: Generate audio query
196+
async with session.post(
197+
f"{base_url}/audio_query",
198+
params={"text": text, "speaker": speaker_id}
199+
) as query_response:
200+
if query_response.status >= 500:
201+
raise aiohttp.ClientResponseError(
202+
request_info=query_response.request_info,
203+
history=query_response.history,
204+
status=query_response.status,
205+
message=f"HTTP {query_response.status}",
206+
headers=query_response.headers
207+
)
208+
query_response.raise_for_status()
209+
audio_query = await query_response.json()
210+
211+
# Step 2: Synthesize audio
212+
async with session.post(
213+
f"{base_url}/synthesis",
214+
params={"speaker": speaker_id},
215+
json=audio_query
216+
) as synthesis_response:
217+
if synthesis_response.status >= 500:
218+
raise aiohttp.ClientResponseError(
219+
request_info=synthesis_response.request_info,
220+
history=synthesis_response.history,
221+
status=synthesis_response.status,
222+
message=f"HTTP {synthesis_response.status}",
223+
headers=synthesis_response.headers
224+
)
225+
synthesis_response.raise_for_status()
226+
wav_bytes = await synthesis_response.read()
227+
228+
elapsed = time.perf_counter() - start_time
229+
230+
# Compute duration and set metric
231+
try:
232+
with wave.open(io.BytesIO(wav_bytes), "rb") as wav_file:
233+
n_frames = wav_file.getnframes()
234+
framerate = wav_file.getframerate()
235+
duration_sec = n_frames / framerate if framerate else 0.0
236+
if duration_sec > 0:
237+
seconds_per_minute = elapsed * 60.0 / duration_sec
238+
else:
239+
seconds_per_minute = 0.0
240+
VOICE_GENERATION_TIME_PER_MINUTE.set(seconds_per_minute)
241+
except Exception:
242+
# 例外は無視して wav_bytes を返す(メトリクスの失敗で処理を止めない)
243+
pass
244+
245+
return base_url, wav_bytes
246+
except aiohttp.ClientResponseError as e:
247+
logging.error(f"VOICEVOX synthesize_bytes failed for URL {base_url} (attempt {attempt+1}/3): {e}")
248+
last_error = e
249+
time.sleep(0.5)
250+
continue
251+
except aiohttp.ClientError as e:
252+
logging.error(f"VOICEVOX synthesize_bytes failed for URL {base_url} (attempt {attempt+1}/3): {e}")
253+
last_error = e
254+
time.sleep(0.5)
255+
continue
256+
except Exception as e:
257+
logging.error(f"VOICEVOX synthesize_bytes unexpected error for URL {base_url}: {e}")
258+
break
259+
# 3回とも失敗した場合のみ次のURLへ
260+
raise RuntimeError(f"All VOICEVOX URLs failed for synthesize_bytes: {text[:50]}... Last error: {last_error}")
207261

208262
# Example usage:
209263
# voicelib = VOICEVOXLib()

0 commit comments

Comments
 (0)