import uuid import discord import os import re import asyncio import logging class MessageReader: # 半角カタカナと数字と全角へのマッピング half_width = "アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲンァィゥェォッャュョー" full_width = "アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲンアイウエオッヤユヨー" translation_table = str.maketrans(half_width, full_width) # 濁点・半濁点を含む半角カタカナの変換マッピング dakuten_map = { 'ガ': 'ガ', 'ギ': 'ギ', 'グ': 'グ', 'ゲ': 'ゲ', 'ゴ': 'ゴ', 'ザ': 'ザ', 'ジ': 'ジ', 'ズ': 'ズ', 'ゼ': 'ゼ', 'ゾ': 'ゾ', 'ダ': 'ダ', 'ヂ': 'ヂ', 'ヅ': 'ヅ', 'デ': 'デ', 'ド': 'ド', 'バ': 'バ', 'ビ': 'ビ', 'ブ': 'ブ', 'ベ': 'ベ', 'ボ': 'ボ', 'パ': 'パ', 'ピ': 'ピ', 'プ': 'プ', 'ペ': 'ペ', 'ポ': 'ポ' } def __init__(self, voice_synthesizer, bot): self.voice_synthesizer = voice_synthesizer self.queue = asyncio.Queue() self.is_reading = False self.bot = bot async def read_message(self, message, word_dictionary, ffmpeg_executable_path, volume=1.0): """メッセージを読み上げるための処理を開始する""" await self.queue.put((message, word_dictionary, ffmpeg_executable_path, volume)) if not self.is_reading: await self._process_queue() async def _process_queue(self): """メッセージキューを処理する""" self.is_reading = True while not self.queue.empty(): message, word_dictionary, ffmpeg_executable_path, volume = await self.queue.get() try: await self._synthesize_and_play(message, word_dictionary, ffmpeg_executable_path, volume) except Exception as e: logging.error(f"Failed to read message: {e}") self.is_reading = False def _convert_to_full_width(self, text): """テキスト内の半角カタカナを全角カタカナに変換する""" for half, full in self.dakuten_map.items(): text = text.replace(half, full) text = text.translate(self.translation_table) return text async def _synthesize_and_play(self, message, word_dictionary, ffmpeg_executable_path, volume): """メッセージの音声合成を行い、再生する""" if not isinstance(message, str): if self.bot.is_name_reading: message.content = f'{message.author.display_name}さん、{message.content}' for user in message.mentions: message.content = message.content.replace(f'<@{user.id}>', user.display_name) message.content = message.content.replace(f'<@!{user.id}>', user.display_name) for role in message.role_mentions: message.content = message.content.replace(f'<@&{role.id}>', role.name) for channel in message.channel_mentions: message.content = message.content.replace(f'<#{channel.id}>', f'#{channel.name}') message.content = re.sub(r'https?://[\w/:%#\$&\?\(\)~\.=\+\\-]+', 'URL省略', message.content) message.content = re.sub(r'<:[a-zA-Z0-9_]+:[0-9]+>', '', message.content) message.content = self._convert_to_full_width(message.content) message.content = message.content.replace('\n', ' ') voice_client = message.guild.voice_client play_message = message.content id = message.id else: voice_client = self.bot.independence_chat.voice_client play_message = message id = str(uuid.uuid4()) if voice_client and voice_client.is_connected(): script = word_dictionary.convert_text(play_message) if len(script) > 140: script = script[:140] output_path = f"{id}.wav" output_path = await self.voice_synthesizer.synthesize(script, output_path=output_path) if not os.path.exists(output_path): logging.error(f"Expected audio file not found: {output_path}") return audio_source = discord.FFmpegPCMAudio(executable=ffmpeg_executable_path, source=output_path) volume_adjusted_source = discord.PCMVolumeTransformer(audio_source, volume=volume) voice_client.play(volume_adjusted_source, after=lambda e: os.remove(output_path)) while voice_client.is_playing(): await asyncio.sleep(1)