88from threading import Timer
99from collections import defaultdict , deque
1010import asyncio
11+ import uuid
12+ import atexit
13+ import signal
14+ import threading
1115
1216# from dotenv import load_dotenv
1317# load_dotenv()
1418
1519queue_dict = defaultdict (deque )
1620connecting_channels = set ()
21+ active_processes = set ()
22+ cleanup_lock = threading .Lock ()
1723
1824dictID = int (os .environ ['DICT_CH_ID' ])
1925dictMsg = None
@@ -36,8 +42,20 @@ def play(voice_client: discord.VoiceClient, queue: deque):
3642 if not queue or voice_client .is_playing ():
3743 return
3844 source = queue .popleft ()
39- # os.remove(source[1])
40- voice_client .play (source [0 ], after = lambda e : play (voice_client , queue ))
45+
46+ def after_play (error ):
47+ if error :
48+ print (f"Player error: { error } " )
49+ # Clean up the audio file after playing
50+ try :
51+ if os .path .exists (source [1 ]):
52+ os .remove (source [1 ])
53+ except Exception as e :
54+ print (f"Failed to remove audio file { source [1 ]} : { e } " )
55+ # Continue playing next item in queue
56+ play (voice_client , queue )
57+
58+ voice_client .play (source [0 ], after = after_play )
4159
4260
4361def current_milli_time () -> int :
@@ -113,25 +131,79 @@ async def jtalk(t) -> str:
113131 htsvoice = ['-m' , '/usr/share/hts-voice/mei/mei_normal.htsvoice' ]
114132 pitch = ['-fm' , '-5' ]
115133 speed = ['-r' , '1.0' ]
116- # file = str(current_milli_time())
117- outwav = ['-ow' , 'output.wav' ]
134+
135+ # Generate unique filename to avoid conflicts
136+ filename = f'output_{ uuid .uuid4 ().hex [:8 ]} .wav'
137+ outwav = ['-ow' , filename ]
118138 cmd = open_jtalk + mech + htsvoice + pitch + speed + outwav
119- c = subprocess .Popen (cmd , stdin = subprocess .PIPE )
120- c .stdin .write (t .encode ())
121- c .stdin .close ()
122- c .wait ()
123- return 'output.wav'
139+
140+ try :
141+ c = subprocess .Popen (cmd , stdin = subprocess .PIPE , stdout = subprocess .PIPE , stderr = subprocess .PIPE )
142+ with cleanup_lock :
143+ active_processes .add (c )
144+
145+ stdout , stderr = c .communicate (input = t .encode (), timeout = 30 )
146+
147+ if c .returncode != 0 :
148+ raise Exception (f"OpenJTalk failed: { stderr .decode ()} " )
149+
150+ return filename
151+ except subprocess .TimeoutExpired :
152+ c .kill ()
153+ raise Exception ("OpenJTalk timeout" )
154+ except Exception as e :
155+ if os .path .exists (filename ):
156+ os .remove (filename )
157+ raise e
158+ finally :
159+ with cleanup_lock :
160+ active_processes .discard (c )
124161
125162
126163def get_voice_client (channel_id : int ) -> discord .VoiceClient | None :
127164 for client in bot .voice_clients :
128- if client .channel .id == channel_id :
165+ if client .channel and client . channel .id == channel_id :
129166 return client
130167 else :
131168 return None
132169
133170
134- async def text_check (text : str , user_name : str ) -> str :
171+ async def check_voice_client_health (voice_client : discord .VoiceClient ) -> bool :
172+ """Check if voice client is healthy and can play audio"""
173+ try :
174+ if not voice_client or not voice_client .is_connected ():
175+ return False
176+ # Test if the voice client is responsive
177+ return True
178+ except Exception as e :
179+ print (f"Voice client health check failed: { e } " )
180+ return False
181+
182+
183+ async def ensure_voice_connection (guild : discord .Guild , channel_id : int ) -> discord .VoiceClient | None :
184+ """Ensure we have a healthy voice connection"""
185+ voice_client = get_voice_client (channel_id )
186+
187+ if voice_client and await check_voice_client_health (voice_client ):
188+ return voice_client
189+
190+ # Reconnect if connection is unhealthy
191+ if voice_client :
192+ try :
193+ await voice_client .disconnect ()
194+ except :
195+ pass
196+
197+ try :
198+ channel = bot .get_channel (channel_id )
199+ if channel :
200+ return await channel .connect ()
201+ except Exception as e :
202+ print (f"Failed to reconnect to voice channel: { e } " )
203+ return None
204+
205+
206+ async def text_check (text : str , user_name : str ) -> tuple [str , str ]:
135207 print (text )
136208 if len (text ) > 150 :
137209 raise Exception ("文字数が長すぎるよ" )
@@ -145,10 +217,17 @@ async def text_check(text: str, user_name: str) -> str:
145217 text = user_name + text
146218 if len (text ) > 150 :
147219 raise Exception ("文字数が長すぎるよ" )
148- filename = await jtalk (text )
149- if os .path .getsize (filename ) > 10000000 :
150- raise Exception ("再生時間が長すぎるよ" )
151- return text , filename
220+
221+ try :
222+ filename = await jtalk (text )
223+ if os .path .getsize (filename ) > 10000000 :
224+ if os .path .exists (filename ):
225+ os .remove (filename )
226+ raise Exception ("再生時間が長すぎるよ" )
227+ return text , filename
228+ except Exception as e :
229+ print (f"TTS generation failed: { e } " )
230+ raise e
152231
153232
154233client_id = os .environ ['DISCORD_CLIENT_ID' ]
@@ -329,16 +408,25 @@ async def on_message(message: discord.Message):
329408 try :
330409 text , filename = await text_check (text , user_name )
331410 except Exception as e :
332- return await message .channel .send (e )
411+ print (f"Text processing error: { e } " )
412+ return await message .channel .send (f"読み上げエラー: { e } " )
333413
334- if not message .guild .voice_client :
414+ # Ensure voice connection is healthy
415+ voice_client = await ensure_voice_connection (message .guild , message .channel .id )
416+ if not voice_client :
417+ print ("Failed to establish voice connection" )
335418 return await bot .process_commands (message )
336419
337- enqueue (message .guild .voice_client , message .guild ,
338- discord .FFmpegPCMAudio (filename ), filename )
339- # timer = Timer(3, os.remove, (filename, ))
340- # timer.start()
341- # os.remove(filename)
420+ try :
421+ enqueue (voice_client , message .guild ,
422+ discord .FFmpegPCMAudio (filename ), filename )
423+ except Exception as e :
424+ print (f"Audio enqueue error: { e } " )
425+ # Clean up file if enqueue fails
426+ if os .path .exists (filename ):
427+ os .remove (filename )
428+ return await message .channel .send ("音声の再生に失敗しました" )
429+
342430 # コマンド側へメッセージ内容を渡す
343431 await bot .process_commands (message )
344432
@@ -353,13 +441,19 @@ async def on_voice_state_update(member: discord.Member, before:discord.VoiceStat
353441 else :
354442 username = member .display_name
355443 if not before .channel and after .channel :
356- filename = await jtalk (username + "さんこんにちは!" )
357- enqueue (member .guild .voice_client , member .guild ,
358- discord .FFmpegPCMAudio (filename ), filename )
444+ try :
445+ filename = await jtalk (username + "さんこんにちは!" )
446+ enqueue (member .guild .voice_client , member .guild ,
447+ discord .FFmpegPCMAudio (filename ), filename )
448+ except Exception as e :
449+ print (f"Failed to play greeting: { e } " )
359450 if before .channel and not after .channel :
360- filename = await jtalk (username + "さんが退出しました" )
361- enqueue (member .guild .voice_client , member .guild ,
362- discord .FFmpegPCMAudio (filename ), filename )
451+ try :
452+ filename = await jtalk (username + "さんが退出しました" )
453+ enqueue (member .guild .voice_client , member .guild ,
454+ discord .FFmpegPCMAudio (filename ), filename )
455+ except Exception as e :
456+ print (f"Failed to play farewell: { e } " )
363457 allbot = True
364458 selfcheck = False
365459 for mem in before .channel .members :
@@ -376,10 +470,38 @@ async def on_voice_state_update(member: discord.Member, before:discord.VoiceStat
376470 await client .disconnect ()
377471 await before .channel .send ('ボイスチャンネルからログアウトしました' )
378472
473+ def cleanup_processes ():
474+ """Clean up any remaining OpenJTalk processes on exit"""
475+ with cleanup_lock :
476+ for process in list (active_processes ):
477+ try :
478+ if process .poll () is None :
479+ process .terminate ()
480+ process .wait (timeout = 5 )
481+ except :
482+ try :
483+ process .kill ()
484+ except :
485+ pass
486+ active_processes .clear ()
487+
488+
489+ # Register cleanup handlers
490+ atexit .register (cleanup_processes )
491+ signal .signal (signal .SIGTERM , lambda signum , frame : cleanup_processes ())
492+ signal .signal (signal .SIGINT , lambda signum , frame : cleanup_processes ())
493+
494+
379495async def main ():
380496 # start the client
381- async with bot :
497+ try :
498+ async with bot :
499+ await bot .start (client_id )
500+ except Exception as e :
501+ print (f"Bot startup failed: { e } " )
502+ finally :
503+ cleanup_processes ()
382504
383- await bot .start (client_id )
384505
385- asyncio .run (main ())
506+ if __name__ == "__main__" :
507+ asyncio .run (main ())
0 commit comments