import re from typing import List, Optional, Tuple from models.bible import Character, Event, Location, StoryBible class DeduplicationService: # Общие слова, которые нужно удалять при нормализации CHARACTER_PREFIXES = [ "котенок", "кот", "господин", "мистер", "доктор", "профессор", "мальчик", "девочка", "пёс", "собака", ] LOCATION_PREFIXES = ["город", "деревня", "село", "улица", "площадь"] def normalize_name(self, name: str, entity_type: str = "character") -> str: """Нормализует имя сущности для сравнения""" # Приводим к нижнему регистру normalized = name.lower().strip() # Убираем лишние пробелы normalized = re.sub(r"\s+", " ", normalized) # Убираем префиксы в зависимости от типа prefixes = ( self.CHARACTER_PREFIXES if entity_type == "character" else self.LOCATION_PREFIXES ) for prefix in prefixes: if normalized.startswith(prefix + " "): normalized = normalized[len(prefix) :].strip() return normalized def is_similar_name(self, name1: str, name2: str) -> bool: """Проверяет, являются ли два имени вариантами одного и того же""" norm1 = self.normalize_name(name1) norm2 = self.normalize_name(name2) # Точное совпадение после нормализации if norm1 == norm2: return True # Одно имя содержит другое (например, "Кекс" и "котенок Кекс") if norm1 in norm2 or norm2 in norm1: return True # Расстояние Левенштейна (простая реализация) distance = self._levenshtein_distance(norm1, norm2) max_len = max(len(norm1), len(norm2)) if max_len > 0: similarity = 1 - (distance / max_len) if similarity > 0.7: # 70% сходства return True return False def _levenshtein_distance(self, s1: str, s2: str) -> int: """Вычисляет расстояние Левенштейна между двумя строками""" if len(s1) < len(s2): return self._levenshtein_distance(s2, s1) if len(s2) == 0: return len(s1) previous_row = range(len(s2) + 1) for i, c1 in enumerate(s1): current_row = [i + 1] for j, c2 in enumerate(s2): insertions = previous_row[j + 1] + 1 deletions = current_row[j] + 1 substitutions = previous_row[j] + (c1 != c2) current_row.append(min(insertions, deletions, substitutions)) previous_row = current_row return previous_row[-1] def find_existing_character( self, name: str, bible: StoryBible ) -> Optional[Character]: """Ищет существующего персонажа с похожим именем""" for char in bible.characters: if self.is_similar_name(name, char.name): return char return None def find_existing_location( self, name: str, bible: StoryBible ) -> Optional[Location]: """Ищет существующую локацию с похожим именем""" for loc in bible.locations: if self.is_similar_name(name, loc.name): return loc return None def find_existing_event(self, title: str, bible: StoryBible) -> Optional[Event]: """Ищет существующее событие с похожим названием""" for event in bible.events: if self.is_similar_name(title, event.title): return event return None def merge_character_data(self, existing: Character, new_data: dict) -> Character: """Объединяет данные нового персонажа с существующим""" # Добавляем новые черты, которых еще нет new_traits = new_data.get("traits", []) for trait in new_traits: if trait and trait not in existing.traits: existing.traits.append(trait) # Обновляем описание, если новое длиннее и содержательнее new_desc = new_data.get("description", "") if new_desc and len(new_desc) > len(existing.description): existing.description = new_desc # Обновляем внешность, если она была пустой new_appearance = new_data.get("appearance", "") if new_appearance and not existing.appearance: existing.appearance = new_appearance return existing # Singleton instance deduplication_service = DeduplicationService()