feat(plagiarism): показывать тематически близкие источники, а не только нарушения
Кандидаты уровня 3 (FAISS), которые прошли порог семантической схожести, но LLM не подтвердила заимствование, раньше молча отбрасывались. Теперь это отдельный блок "recommendations" в отчёте — не плагиат, но источники, полезные для раскрытия темы.
This commit is contained in:
@@ -60,6 +60,7 @@ def aggregate_results(
|
||||
semantic_matches: list[dict[str, Any]],
|
||||
total_fragments: int,
|
||||
full_text: str = "",
|
||||
related_candidates: list[dict[str, Any]] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Свести совпадения уровней в итог проверки.
|
||||
|
||||
@@ -77,11 +78,15 @@ def aggregate_results(
|
||||
total_fragments: всего проверенных фрагментов документа
|
||||
full_text: полный текст проверяемого документа — нужен для is_cited;
|
||||
пустая строка → ни один фрагмент не размечается как цитата
|
||||
related_candidates: кандидаты уровня 3 (FAISS), которые прошли порог
|
||||
семантической схожести, но LLM не подтвердила парафраз/плагиат —
|
||||
не заимствование, но тематически близкая работа. Используются
|
||||
для "recommendations", а не для процента схожести.
|
||||
|
||||
Returns:
|
||||
dict с полями overall_similarity, uncited_similarity, matches (с полем
|
||||
"cited" в каждом), total_fragments, flagged_fragments, cited_fragments,
|
||||
uncited_fragments, by_method.
|
||||
uncited_fragments, by_method, recommendations.
|
||||
"""
|
||||
all_matches = level1_matches + level2_matches + semantic_matches
|
||||
|
||||
@@ -101,6 +106,18 @@ def aggregate_results(
|
||||
pct = (len(positions) / total_fragments * 100) if total_fragments > 0 else 0.0
|
||||
return round(min(pct, 100.0), 2)
|
||||
|
||||
# Рекомендации: лучший кандидат на источник (без уже засчитанных как совпадение),
|
||||
# отсортированы по убыванию схожести, не более 10 — чтобы не захламлять отчёт.
|
||||
flagged_sources = {m.get("source_url") or m.get("source_title") for m in semantic_matches}
|
||||
best_by_source: dict[str, dict[str, Any]] = {}
|
||||
for c in related_candidates or []:
|
||||
key = c.get("source_url") or c.get("source_title", "")
|
||||
if not key or key in flagged_sources:
|
||||
continue
|
||||
if key not in best_by_source or c.get("similarity", 0) > best_by_source[key].get("similarity", 0):
|
||||
best_by_source[key] = c
|
||||
recommendations = sorted(best_by_source.values(), key=lambda c: c.get("similarity", 0), reverse=True)[:10]
|
||||
|
||||
return {
|
||||
"overall_similarity": _pct(flagged_positions),
|
||||
"uncited_similarity": _pct(uncited_positions),
|
||||
@@ -114,4 +131,5 @@ def aggregate_results(
|
||||
"fuzzy": len(level2_matches),
|
||||
"semantic_llm": len(semantic_matches),
|
||||
},
|
||||
"recommendations": recommendations,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user