import os from logging import Logger from tempfile import NamedTemporaryFile from pyrogram import filters from pyrogram.filters import Filter from pyrogram.client import Client from pyrogram.types import Message from pyrogram.enums import ChatAction from src.bot.handlers import AbstractCommandHandler from src.utils import audio_processing class VoiceCommandHandler(AbstractCommandHandler): """ Command handler for the /voice command in a Pyrogram bot. This handler processes voice messages, converts them to text in a specified language, and sends the result back to the user. Attributes: COMMAND (`str`): The name of the command that this handler handles. LANGUAGE_ALIASES (`dict`): Mapping of language codes to their aliases. DEFAULT_LANGUAGE (`str`): Default language code for conversion if no language is specified. MAX_DURATION (`int`): Maximum allowed duration of the voice message in seconds. logger (`Logger`): Logger instance for logging events. """ # Mapping of language codes to their aliases LANGUAGE_ALIASES: dict[str, list[str]] = { "en": ["en", "eng", "english"], "ru": ["ru", "rus", "russian"], } # Default language code for conversion if no language is specified DEFAULT_LANGUAGE: str = "ru" # Maximum allowed duration of the voice message in seconds MAX_DURATION: int = 300 def __init__(self, logger: Logger) -> None: """ Initializes the VoiceCommandHandler. Args: logger (`Logger`): Logger instance for logging events. """ self.logger: Logger = logger @property def COMMAND(self) -> str: """ The name of the command that this handler handles. Returns: str: The command name. """ return "voice" def get_filters(self) -> Filter: """ Returns the filter for the /voice command. Returns: `pyrogram.filters.Filter`: A Pyrogram filter matching the /voice command. """ return filters.command(self.COMMAND) async def handle(self, client: Client, message: Message) -> None: """ Handles the /voice command. Converts a voice message to text in the specified language and sends the result back to the user. The command must be used in reply to a voice message. Args: client (`pyrogram.client.Client`): The Pyrogram client instance. message (`pyrogram.types.Message`): The incoming message object to process. """ self.logger.info(f"Received /{self.COMMAND} command from chat_id={message.chat.id}.") # Parse the language parameter or use the default (Russian) command_arguments: list[str] = message.text.split() try: language_code: str = ( self.__resolve_language(command_arguments[1]) if len(command_arguments) > 1 else self.DEFAULT_LANGUAGE ) except ValueError as error: await message.reply(str(error), quote=True) return if not (message.reply_to_message and message.reply_to_message.voice): self.logger.warning(f"The /{self.COMMAND} command was not used in reply to a voice message.") await message.reply("Please reply to a voice message with the /voice command.", quote=True) return # Notify the user that the request is being processed processing_message: Message = await message.reply_to_message.reply( "Converting voice message to text...", quote=True ) with NamedTemporaryFile(delete=False) as temp_audio_file: audio_file_path = await client.download_media( message.reply_to_message.voice.file_id, file_name=temp_audio_file.name ) self.logger.info(f"Voice message downloaded to {audio_file_path}.") try: # Validate voice message duration voice_duration: float = audio_processing.get_audio_duration(audio_file_path) # type: ignore if voice_duration > self.MAX_DURATION: self.logger.warning(f"Voice message too long: {voice_duration} seconds.") await processing_message.edit_text( f"The voice message is too long (over {self.MAX_DURATION // 60} minutes). Please send a shorter one." ) return await client.send_chat_action(message.chat.id, ChatAction.TYPING) extracted_text: str = audio_processing.convert_voice_to_text(audio_file_path, language=language_code) # type: ignore self.logger.info("Voice message successfully converted to text.") response_text: str = ( f"<pre language=\"Conversion Result ({language_code})\">" f"{extracted_text}" "</pre>" ) await processing_message.edit_text(response_text) except FileNotFoundError: self.logger.error("File not found during processing.", exc_info=True) await processing_message.edit_text("An error occurred while processing the voice message. Please try again later.") except RuntimeError: self.logger.error("A runtime error occurred.", exc_info=True) await processing_message.edit_text("An error occurred while processing the voice message. Please try again later.") except Exception: self.logger.error("An unexpected error occurred.", exc_info=True) await processing_message.edit_text("An error occurred while processing the voice message. Please try again later.") finally: await client.send_chat_action(message.chat.id, ChatAction.CANCEL) if os.path.exists(audio_file_path): # type: ignore os.remove(audio_file_path) # type: ignore def __resolve_language(self, language_input: str) -> str: """ Resolves the language code based on the input text. Args: language_input (str): User-provided language parameter. Returns: str: The resolved language code. Raises: ValueError: If the input does not match any supported language. """ normalized_input: str = language_input.lower() for language_code, aliases in self.LANGUAGE_ALIASES.items(): if normalized_input in aliases: return language_code raise ValueError( "Invalid language parameter. Please use one of the following:\n" + "\n".join(f"{language_code}: {', '.join(aliases)}" for language_code, aliases in self.LANGUAGE_ALIASES.items()) )