initial commit
Build and Push Docker Images / build-and-push (push) Failing after 3m33s

This commit is contained in:
admin committed 2026-11-07 16:39:43 +03:00
commit 46af290db3
100 files changed
+16663

No files matched your search

+133
View File
@@ -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()