From 337e642b0a1fe7f81b3d4ff03d9174b59e4bfd2e Mon Sep 17 00:00:00 2001 From: Asaf Kali Date: Fri, 8 Nov 2024 16:05:58 +0200 Subject: [PATCH] Parsing structure progress --- app/bot/handlers/parse/__init__.py | 0 app/bot/handlers/parse/parse_board_handler.py | 36 ++++++ .../handlers/parse/parse_fixing_handler.py | 13 +++ app/bot/handlers/parse/parse_handler.py | 28 +++++ .../handlers/parse/parse_language_handler.py | 25 +++++ app/bot/handlers/parse/parse_map_handler.py | 40 +++++++ app/bot/handlers/parse/photos.py | 21 ++++ app/bot/handlers/parse_handler.py | 106 ------------------ 8 files changed, 163 insertions(+), 106 deletions(-) create mode 100644 app/bot/handlers/parse/__init__.py create mode 100644 app/bot/handlers/parse/parse_board_handler.py create mode 100644 app/bot/handlers/parse/parse_fixing_handler.py create mode 100644 app/bot/handlers/parse/parse_handler.py create mode 100644 app/bot/handlers/parse/parse_language_handler.py create mode 100644 app/bot/handlers/parse/parse_map_handler.py create mode 100644 app/bot/handlers/parse/photos.py delete mode 100644 app/bot/handlers/parse_handler.py diff --git a/app/bot/handlers/parse/__init__.py b/app/bot/handlers/parse/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/bot/handlers/parse/parse_board_handler.py b/app/bot/handlers/parse/parse_board_handler.py new file mode 100644 index 0000000..1a19a37 --- /dev/null +++ b/app/bot/handlers/parse/parse_board_handler.py @@ -0,0 +1,36 @@ +import requests +from bot.config import get_config +from bot.handlers.other.event_handler import EventHandler, build_board_keyboard +from bot.handlers.parse.photos import _get_base64_photo +from bot.models import BotState +from codenames.game.board import Board +from codenames.game.card import Card + +# Board -> Fixing + + +class ParseBoardHandler(EventHandler): + def handle(self): + photo_base64 = _get_base64_photo(photos=self.update.message.photo) + self.send_text("Working on it. This might take a minute ⏳️") + parsing_state = self.session.parsing_state + parsed_words = _parse_board_words(photo_base64=photo_base64, language=parsing_state.language) + words = [word if word else str(i) for i, word in enumerate(parsed_words)] + cards = [Card(word=word, color=color) for word, color in zip(words, parsing_state.card_colors)] + parsed_board = Board(language=parsing_state.language, cards=cards) + keyboard = build_board_keyboard(table=parsed_board.as_table, is_game_over=True) + message = "πŸŽ‰ Done! Here's the board.\nClick on any card to fix it. When you are done, send me /done." + text = self.send_markdown(text=message, reply_markup=keyboard) + self.update_session(last_keyboard_message_id=text.message_id, parsing_state=None) + return BotState.PARSE_FIXES + + +def _parse_board_words(photo_base64: str, language: str) -> list[str]: + env_config = get_config() + url = f"{env_config.base_parser_url}/parse-board" + payload = {"board_image_b64": photo_base64, "language": language} + response = requests.get(url=url, json=payload, timeout=80) + response.raise_for_status() + response_json = response.json() + words = response_json.get("words") + return words diff --git a/app/bot/handlers/parse/parse_fixing_handler.py b/app/bot/handlers/parse/parse_fixing_handler.py new file mode 100644 index 0000000..c0e926a --- /dev/null +++ b/app/bot/handlers/parse/parse_fixing_handler.py @@ -0,0 +1,13 @@ +from bot.handlers.other.event_handler import EventHandler +from bot.models import BotState + +# Fixing + + +class ParseFixesHandler(EventHandler): + def handle(self): + text = self.update.message.text.lower().strip() + if text == "/done": + return + self.send_text("🧩 Please send me a picture of the fixed board:") + return BotState.PARSE_BOARD diff --git a/app/bot/handlers/parse/parse_handler.py b/app/bot/handlers/parse/parse_handler.py new file mode 100644 index 0000000..05385c3 --- /dev/null +++ b/app/bot/handlers/parse/parse_handler.py @@ -0,0 +1,28 @@ +from bot.handlers.other.event_handler import EventHandler +from bot.models import BotState, ParsingState +from the_spymaster_util.logger import get_logger + +log = get_logger(__name__) + +# Entrypoint -> Language + +LANGUAGES_CODES = { + "hebrew": "heb", + "hnglish": "eng", + "hussian": "rus", +} + + +class ParseHandler(EventHandler): + def handle(self): + keyboard = _build_language_options_keyboard() + parsing_state = ParsingState() + self.update_session(parsing_state=parsing_state) + # Language selection + self.send_markdown("πŸ”€ Pick cards language:", reply_markup=keyboard) + return BotState.PARSE_LANGUAGE + + +def _build_language_options_keyboard() -> list[str]: + keyboard = [language.title() for language in LANGUAGES_CODES.keys()] + return keyboard diff --git a/app/bot/handlers/parse/parse_language_handler.py b/app/bot/handlers/parse/parse_language_handler.py new file mode 100644 index 0000000..d32c595 --- /dev/null +++ b/app/bot/handlers/parse/parse_language_handler.py @@ -0,0 +1,25 @@ +from bot.handlers.other.event_handler import EventHandler +from bot.handlers.parse.parse_handler import LANGUAGES_CODES, log +from bot.models import BotState + +# Language -> Map + + +class ParseLanguageHandler(EventHandler): + def handle(self): + text = self.update.message.text.lower() + language_code = _get_language_code(text) + log.info(f"Setting language: '{language_code}'") + self.update_parsing_state(language=language_code) + # Map parsing + self.send_text("πŸ—ΊοΈ Please send me a picture of the map:") + return BotState.PARSE_MAP + + +def _get_language_code(text: str) -> str: + text = text.lower().strip() + if text in LANGUAGES_CODES: + return LANGUAGES_CODES[text] + log.info(f"Unknown language: '{text}'") + language_code = text[:3] + return language_code diff --git a/app/bot/handlers/parse/parse_map_handler.py b/app/bot/handlers/parse/parse_map_handler.py new file mode 100644 index 0000000..a02e59a --- /dev/null +++ b/app/bot/handlers/parse/parse_map_handler.py @@ -0,0 +1,40 @@ +import requests +from bot.config import get_config +from bot.handlers.other.event_handler import EventHandler +from bot.handlers.parse.photos import _get_base64_photo +from bot.models import BotState, ParsingState +from codenames.game.color import CardColor + + +# Map -> Board +class ParseMapHandler(EventHandler): + def handle(self): + photo_base64 = _get_base64_photo(photos=self.update.message.photo) + card_colors = _parse_map_colors(photo_base64) + self._send_as_emoji_table(card_colors) + parsing_state = ParsingState(language="heb", card_colors=card_colors) + self.update_session(parsing_state=parsing_state) + # Board parsing + self.send_text("🧩 Please send me a picture of the board:") + return BotState.PARSE_BOARD + + def _send_as_emoji_table(self, card_colors: list[CardColor]): + result = "I got: \n\n" + for i in range(0, len(card_colors), 5): + row = card_colors[i : i + 5] + row_emojis = " ".join(card.emoji for card in row) + result += f"{row_emojis}\n" + result += "\nYou will have a chance to fix any mistakes later." + self.send_text(result) + + +def _parse_map_colors(photo_base64: str) -> list[CardColor]: + env_config = get_config() + url = f"{env_config.base_parser_url}/parse-color-map" + payload = {"map_image_b64": photo_base64} + response = requests.get(url=url, json=payload, timeout=15) + response.raise_for_status() + response_json = response.json() + map_colors = response_json.get("map_colors") + card_colors = [CardColor(color) for color in map_colors] + return card_colors diff --git a/app/bot/handlers/parse/photos.py b/app/bot/handlers/parse/photos.py new file mode 100644 index 0000000..aa95cd7 --- /dev/null +++ b/app/bot/handlers/parse/photos.py @@ -0,0 +1,21 @@ +import base64 + +from bot.handlers.parse.parse_handler import log +from bot.models import BadMessageError +from telegram import PhotoSize + + +def _get_base64_photo(photos: list[PhotoSize]) -> str: + if not photos: + raise BadMessageError("No photo found in message") + log.info(f"Got {len(photos)} photos, downloading the largest one") + photo_meta = _pick_largest_photo(photos) + photo_ptr = photo_meta.get_file() + photo_bytes = photo_ptr.download_as_bytearray() + photo_base64 = base64.b64encode(photo_bytes).decode("utf-8") + log.info("Downloaded and encoded photo") + return photo_base64 + + +def _pick_largest_photo(photos: list[PhotoSize]) -> PhotoSize: + return max(photos, key=lambda photo: photo.file_size) diff --git a/app/bot/handlers/parse_handler.py b/app/bot/handlers/parse_handler.py deleted file mode 100644 index 4ceb9f6..0000000 --- a/app/bot/handlers/parse_handler.py +++ /dev/null @@ -1,106 +0,0 @@ -import base64 - -import requests -from bot.config import get_config -from bot.handlers.base import EventHandler, build_board_keyboard -from bot.models import BadMessageError, BotState, ParsingState -from codenames.game.board import Board -from codenames.game.card import Card -from codenames.game.color import CardColor -from telegram import PhotoSize -from the_spymaster_util.logger import get_logger - -log = get_logger(__name__) - - -class ParseHandler(EventHandler): - def handle(self): - self.send_text("πŸ—ΊοΈ Please send me a picture of the map:") - return BotState.PARSE_MAP - - -class ParseMapHandler(EventHandler): - def handle(self): - photo_base64 = _get_base64_photo(photos=self.update.message.photo) - card_colors = _parse_map_colors(photo_base64) - self._send_as_emoji_table(card_colors) - parsing_state = ParsingState(language="heb", card_colors=card_colors) - self.update_session(parsing_state=parsing_state) - self.send_text("🧩 Please send me a picture of the board:") - return BotState.PARSE_BOARD - - def _send_as_emoji_table(self, card_colors: list[CardColor]): - result = "I got: \n\n" - for i in range(0, len(card_colors), 5): - row = card_colors[i : i + 5] - row_emojis = " ".join(card.emoji for card in row) - result += f"{row_emojis}\n" - result += "\nYou will have a chance to fix any mistakes later." - self.send_text(result) - - -class ParseBoardHandler(EventHandler): - def handle(self): - photo_base64 = _get_base64_photo(photos=self.update.message.photo) - self.send_text("Working on it. This might take a minute ⏳️") - parsing_state = self.session.parsing_state - words = _parse_board_words(photo_base64=photo_base64, language=parsing_state.language) - words = [word if word else "???" for word in words] - cards = [Card(word=word, color=color) for word, color in zip(words, parsing_state.card_colors)] - parsed_board = Board(language=parsing_state.language, cards=cards) - keyboard = build_board_keyboard(table=parsed_board.as_table, is_game_over=True) - text = self.send_markdown("πŸŽ‰ Done! Here's the board:", reply_markup=keyboard) - self.update_session(last_keyboard_message_id=text.message_id, parsing_state=None) - return BotState.PARSE_BOARD - - def _send_as_table(self, words: list[str]): - result = "OK! I got: \n\n" - for i in range(0, len(words), 5): - row = words[i : i + 5] - row_str = " | ".join(_word_cell(word) for word in row) - result += f"{row_str}\n" - result += "\nHow does it look?" - self.send_markdown(result) - - -def _word_cell(word: str) -> str: - return f"*{word:<5}*" - - -def _parse_map_colors(photo_base64: str) -> list[CardColor]: - env_config = get_config() - url = f"{env_config.base_parser_url}/parse-color-map" - payload = {"map_image_b64": photo_base64} - response = requests.get(url=url, json=payload, timeout=15) - response.raise_for_status() - response_json = response.json() - map_colors = response_json.get("map_colors") - card_colors = [CardColor(color) for color in map_colors] - return card_colors - - -def _parse_board_words(photo_base64: str, language: str) -> list[str]: - env_config = get_config() - url = f"{env_config.base_parser_url}/parse-board" - payload = {"board_image_b64": photo_base64, "language": language} - response = requests.get(url=url, json=payload, timeout=80) - response.raise_for_status() - response_json = response.json() - words = response_json.get("words") - return words - - -def _get_base64_photo(photos: list[PhotoSize]) -> str: - if not photos: - raise BadMessageError("No photo found in message") - log.info(f"Got {len(photos)} photos, downloading the largest one") - photo_meta = _pick_largest_photo(photos) - photo_ptr = photo_meta.get_file() - photo_bytes = photo_ptr.download_as_bytearray() - photo_base64 = base64.b64encode(photo_bytes).decode("utf-8") - log.info("Downloaded and encoded photo") - return photo_base64 - - -def _pick_largest_photo(photos: list[PhotoSize]) -> PhotoSize: - return max(photos, key=lambda photo: photo.file_size)