o2o-plagiarism-ai/tests/test_summarizer.py
hbyang 8ae52b0a82 feat: add summary (No.7) consensus and grounded generation trials
요약 성능지표(No.7, ROUGE 65점) 실험 일습을 커밋한다.

- app/engine/summary_consensus.py: 후보 5개 생성 후 합의 선택(select_consensus).
  문자/어절 2-gram 가중 합의로 고르며, 유효 후보가 없으면 valid=False 를 낸다.
- app/engine/summary_grounded.py: 근거 추출 후 압축하는 2단계 생성.
- summarizer.py: 추출 전략 3종(textrank/coverage/lead). coverage 는 MMR 로
  중복 문장을 눌러 문서 전체를 넓게 담는다. 기본값은 textrank 로 유지한다.

스크립트는 생성·평가·검증을 분리했다. verify_* 는 API 호출 없이 저장된 산출물만
재계산하는 독립 검증기라 지표를 공용 함수로 합치지 않는다. 합치면 검증이 성립하지
않는다. tune_summarizer 는 사람 검수 참조(build_summary_annotation_packet ->
export_summary_annotations)를 입력으로 받아 선택과 최종 보고를 분리한다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-28 10:02:04 +09:00

94 lines
3.5 KiB
Python

"""스토리 요약 모듈 단위테스트 (계획서 과제2 ②).
추출적 요약은 외부 의존(LLM/정답셋) 없이 동작해야 한다.
"""
from __future__ import annotations
from app.core.config import Settings
from app.engine.summarizer import (
Summarizer,
extractive_summary,
split_sentences,
)
_DOC = (
"홍길동은 조선시대 의적이었다. 그는 활빈당을 만들어 탐관오리의 재물을 빼앗았다. "
"빼앗은 재물은 가난한 백성들에게 나누어 주었다. 홍길동은 둔갑술과 도술에 능했다. "
"조정에서는 그를 잡으려 했으나 번번이 실패했다. 결국 홍길동은 율도국으로 떠나 새 나라를 세웠다. "
"오늘 날씨는 맑고 화창하다. 점심으로 김밥을 먹었다."
)
def test_split_sentences():
sents = split_sentences("첫 문장이다. 둘째 문장이다! 셋째 문장인가?")
assert len(sents) == 3
def test_extractive_summary_shortens():
result = extractive_summary(_DOC, ratio=0.4)
assert result.num_sentences_in == 8
assert 0 < result.num_sentences_out < result.num_sentences_in
assert result.mode == "extractive"
assert result.final == result.extractive
def test_extractive_preserves_source_order():
result = extractive_summary(_DOC, ratio=0.5)
# 선택된 인덱스는 원문 등장 순서여야 함 (가독성)
assert result.selected_indices == sorted(result.selected_indices)
def test_extractive_picks_central_over_offtopic():
"""본문 핵심(홍길동 서사)이 무관한 문장(날씨/점심)보다 우선 선택."""
result = extractive_summary(_DOC, ratio=0.4)
text = result.extractive
assert "홍길동" in text
# 마지막 두 무관 문장은 덜 선택되어야 함
assert not ("김밥" in text and "날씨" in text)
def test_max_sentences_cap():
result = extractive_summary(_DOC, ratio=1.0, max_sentences=2)
assert result.num_sentences_out <= 2
def test_detail_controls_default_length():
settings = Settings(use_llm_extractor=False, openai_api_key="", use_kosimcse=False)
summarizer = Summarizer(settings)
brief = summarizer.summarize(_DOC, detail="brief")
detailed = summarizer.summarize(_DOC, detail="detailed")
assert brief.num_sentences_out < detailed.num_sentences_out
def test_emphasis_prioritizes_requested_topic():
result = extractive_summary(_DOC, ratio=0.25, emphasis=["김밥"])
assert "김밥" in result.extractive
assert result.emphasis == ["김밥"]
def test_coverage_strategy_preserves_source_order_and_respects_cap():
result = extractive_summary(_DOC, ratio=0.5, max_sentences=3, strategy="coverage")
assert result.selected_indices == sorted(result.selected_indices)
assert result.num_sentences_out == 3
def test_unknown_extractive_strategy_is_rejected():
import pytest
with pytest.raises(ValueError):
extractive_summary(_DOC, strategy="oracle")
def test_empty_and_short():
assert extractive_summary("").final == ""
one = extractive_summary("한 문장만 있다.")
assert one.final == "한 문장만 있다."
def test_summarizer_without_llm_is_extractive():
"""LLM 비활성 시 통합 요청도 추출적 결과를 반환 (외부 의존 0 폴백)."""
settings = Settings(use_llm_extractor=False, openai_api_key="", use_kosimcse=False)
result = Summarizer(settings).summarize(_DOC, ratio=0.4, use_abstractive=True)
assert result.mode == "extractive"
assert result.abstractive is None
assert result.final