This commit is contained in:
commit
46af290db3
100 files changed
+16663
No files matched your search
@@ -0,0 +1,133 @@
|
||||
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()
|
||||
Reference in new issue
Block a user