diff --git a/resources/main.cfg b/resources/main.cfg index e15a8e2..77b18ba 100644 --- a/resources/main.cfg +++ b/resources/main.cfg @@ -12,12 +12,13 @@ level=INFO [grammar] chain_len=2 -separator=\x02 +sep=\x02 stop_word=\x00 max_wrds=30 max_msgs=5 -endsent=.....!!? +endsen=.....!!? garbage=«<{([.!?;\-—"/*&^#$|#$%^&*(№;%:?*])}>» +garbage_entities=mention,text_mention,bot_command,url,email [media_checker] lifetime=28800.0 diff --git a/src/config.py b/src/config.py index a15d0f1..b7fcfb3 100644 --- a/src/config.py +++ b/src/config.py @@ -6,7 +6,7 @@ encoding = 'utf-8' sections = { 'bot': ['token', 'name', 'anchors', 'god_mode', 'purge_interval', 'default_chance', 'spam_stickers'], - 'grammar': ['chain_length', 'separator', 'stop_word', 'end_sentence', 'all'], + 'grammar': ['chain_len', 'sep', 'stop_word', 'max_wrds', 'max_msgs', 'endsen', 'garbage', 'garbage_entities'], 'logging': ['level'], 'updates': ['mode'], 'media_checker': ['lifetime', 'messages'], diff --git a/src/service/reply_generator.py b/src/service/reply_generator.py index a806a9f..38d489a 100644 --- a/src/service/reply_generator.py +++ b/src/service/reply_generator.py @@ -15,7 +15,7 @@ class ReplyGenerator: self.max_msgs = config.getint('grammar', 'max_msgs') self.stop_word = config['grammar']['stop_word'] - self.separator = config['grammar']['separator'] + self.sep = config['grammar']['sep'] self.endsen = config['grammar']['endsen'] def generate(self, message): @@ -25,15 +25,25 @@ class ReplyGenerator: :param message: Message :return: - response (a message) - - empty string (if response == message) + - None (if response == message or no message chains was generated) """ words = self.tokenizer.extract_words(message) # TODO explain this + """ + Преобразовываем триграммы в пары слов, чтобы было проще составлять цепочку. + Например [('hello', 'how', 'do'), ('do', 'you', 'feel'), ('feel', 'today', None)] станет + вот таким [('hello', 'how'), ('do', 'you'), ('feel', 'today')] + """ pairs = [trigram[:-1] for trigram in self.tokenizer.split_to_trigrams(words)] - + # TODO explain why it returns what it returns + """ + Генерирует цепочку для КАЖДОЙ пары слов. + Причины для возвращения самого длинного сообщения из всех - нет, + на тот момент казалось что это наилучший вариант + """ messages = [self.__generate_best_message(chat_id=message.chat_id, pair=pair) for pair in pairs] longest_message = max(messages, key=len) if len(messages) else None @@ -44,34 +54,54 @@ class ReplyGenerator: def __generate_best_message(self, chat_id, pair): # TODO explain this method + """ + Метод генерирует несколько (максимум self.max_msgs) различных предложений, + и оставляет лишь то, которое длиннее всех + """ best_message = '' for _ in range(self.max_msgs): generated = self.__generate_sentence(chat_id=chat_id, pair=pair) if len(generated) > len(best_message): best_message = generated - # TODO explain the concept of the BEST message + # TODO explain the concept of the BEST message + """ + У кого длиннее - тот и победил. Самый тупой алгоритм на свете + """ return best_message def __generate_sentence(self, chat_id, pair): # TODO explain this method gen_words = [] - key = self.separator.join(pair) + key = self.sep.join(pair) # TODO explain this loop + """ + Например приходит пара (привет, бот) + Мы ее преобразуем в строку 'привет$бот' и пишем в key. + Затем итерируемся максимум 50 раз. На каждой итерации: + + 1. Преобразуем ключ обратно в пару, 'привет$бот' станет вновь (привет, бот) + 2. Добавляем в gen_words второе или первое слово из пары, в зависимости от размера gen_words (Вот это как раз и формирует результат) + 3. Получаем случайное слово используя пару привет$бот. + 4.1. Если слово не нашлось, то прерываем цикл, это означает что у этой пары нет соотношений в базе + 4.2. Если слово нашлось, например 'пидор' то формируем новый ключ из пары, например тут станет бот$пидор (key ВСЕГДА будет состоять из 2-х слов) и повторяем + """ for _ in range(self.max_wrds): - words = key.split(self.separator) + words = key.split(self.sep) - gen_words.append(words[1] if len(gen_words) == 0 else words[1]) + # Исправил ошибку тут !!!!!! + gen_words.append(words[0] if len(gen_words) == 0 else words[1]) next_word = self.trigram_repository.get_random_reply(chat_id, key) if next_word is None: break - key = self.separator.join(words[1:] + [next_word]) + key = self.sep.join(words[1:] + [next_word]) # TODO explain what's last word + # Самое последнее слово в сегенирированном ключе # Append last word unless it is in the list already - last_word = key.split(self.separator)[-1] + last_word = key.split(self.sep)[-1] if last_word not in gen_words: gen_words.append(last_word) @@ -86,8 +116,18 @@ class ReplyGenerator: sentence = ' '.join(gen_words).strip() if sentence[-1:] not in self.endsen: # TODO explain this pls: + """ + Если в конце сформированного предложения нету пунктуации (?!.) (мы ведь не вырезаем эти символы), + то добавляем ему случайную (?!.) из config.grammar.endsen + """ sentence += self.tokenizer.random_end_sentence_token() - # sentence = capitalize(sentence) + # sentence = capitalize(sentence) + """ + Чем плохо начало предложения с большой буквы? + """ # TODO my intuition tells me we shouldn't return fun(obj), but IDK really + """ + Не знал о таком стандарте в питоне или что это плохо, зачем лишняя переменная? + """ return sentence diff --git a/src/tokenizer.py b/src/tokenizer.py index 187009d..2d39beb 100644 --- a/src/tokenizer.py +++ b/src/tokenizer.py @@ -7,49 +7,66 @@ class Tokenizer: def __init__(self): self.chain_len = config.getint('grammar', 'chain_len') self.stop_word = config['grammar']['stop_word'] - self.endsent = config['grammar']['endsent'] + self.endsen = config['grammar']['endsen'] self.garbage = config['grammar']['garbage'] + # https://core.telegram.org/bots/api#messageentity + self.garbage_entities = config.getlist('grammar', 'garbage_entities') - def split_to_trigrams(self, src_words): - if len(src_words) <= self.chain_len: + def split_to_trigrams(self, words_list): + if len(words_list) <= self.chain_len: yield from () words = [self.stop_word] - for word in src_words: + for word in words_list: words.append(word) - if word[-1] in self.endsent: + if word[-1] in self.endsen: words.append(self.stop_word) if words[-1] != self.stop_word: words.append(self.stop_word) for i in range(len(words) - self.chain_len): j = i + self.chain_len + 1 - yield words[i : j] + yield words[i:j] def extract_words(self, message): - symbols = list(re.sub('\s', ' ', message.text)) + symbols = list(re.sub('\s', ' ', self.remove_garbage_entities(message))) - for entity in message.entities: - # TODO: explain the code - # TODO: validate the formula - symbols[entity.offset : (entity.length+entity.offset)] = ' ' * entity.length - - return list(filter(None, map(self.__prettify, ''.join(symbols).split(' ')))) + return list(filter(None, map(self.prettify, ''.join(symbols).split(' ')))) def random_end_sentence_token(self): - return random_element(list(self.endsent)) + return random_element(list(self.endsen)) - def __prettify(self, word): + def remove_garbage_entities(self, message): + encoding = 'utf-16-le' + utf16bytes = message.text.encode(encoding) + result = bytearray() + cur_pos = 0 + + for e in message.entities: + start_pos = e.offset * 2 + end_pos = (e.offset + e.length) * 2 + + result += utf16bytes[cur_pos:start_pos] + if e.type not in self.garbage_entities: + result += utf16bytes[start_pos:end_pos] + + cur_pos = end_pos + + result += utf16bytes[cur_pos:] + + return utf16bytes.decode(encoding) + + def prettify(self, word): lowercase_word = word.lower().strip() last_symbol = lowercase_word[-1:] - if last_symbol not in self.endsent: + if last_symbol not in self.endsen: last_symbol = '' - pretty_word = lowercase_word.strip(self.garbage_tokens) + pretty_word = lowercase_word.strip(self.garbage) if pretty_word != '' and len(pretty_word) > 2: return pretty_word + last_symbol - elif lowercase_word in self.garbage_tokens: + elif lowercase_word in self.garbage: return None return lowercase_word