"""Чистая доменная логика скоринга плагиата — без Celery/БД/сети. Объединяет совпадения всех уровней (1 winnowing, 2 minhash, 3+4 semantic+llm), дедуплицирует их и считает итоговый процент схожести, который видит студент. Вынесено из Celery-задачи, чтобы логику можно было тестировать изолированно. """ from typing import Any def aggregate_results( level1_matches: list[dict[str, Any]], level2_matches: list[dict[str, Any]], semantic_matches: list[dict[str, Any]], total_fragments: int, ) -> dict[str, Any]: """Свести совпадения уровней в итог проверки. Дедупликация — по паре (source_title, position_start): одно и то же совпадение источника в одной позиции не дублируется. Итоговый процент — доля УНИКАЛЬНЫХ помеченных позиций от всех фрагментов (фрагмент, совпавший с несколькими источниками, не раздувает процент выше 100). Args: level1_matches: совпадения уровня 1 (Winnowing, точные) level2_matches: совпадения уровня 2 (MinHash, нечёткие) semantic_matches: совпадения уровней 3-4 (FAISS + LLM-парафраз) total_fragments: всего проверенных фрагментов документа Returns: dict с полями overall_similarity, matches, total_fragments, flagged_fragments, by_method. """ all_matches = level1_matches + level2_matches + semantic_matches seen: set[str] = set() unique_matches: list[dict[str, Any]] = [] for m in all_matches: key = f"{m.get('source_title', '')}:{m.get('position_start', '')}" if key not in seen: seen.add(key) unique_matches.append(m) flagged_positions = {m.get("position_start") for m in unique_matches} flagged_frags = len(flagged_positions) overall = (flagged_frags / total_fragments * 100) if total_fragments > 0 else 0.0 overall = min(overall, 100.0) return { "overall_similarity": round(overall, 2), "matches": unique_matches, "total_fragments": total_fragments, "flagged_fragments": flagged_frags, "by_method": { "exact": len(level1_matches), "fuzzy": len(level2_matches), "semantic_llm": len(semantic_matches), }, }