134 lines
5.1 KiB
Python
134 lines
5.1 KiB
Python
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()
|