컴북스 데이터 수령 전 가능한 작업을 선행 구현. 데이터 도착 즉시 학습·검증에 착수할 수 있도록 알고리즘·평가 하니스·문서를 준비. 알고리즘/파이프라인: - engine/clustering.py: 군집화 기반 부분 표절(요소 교체) 판별, detect 응답에 partial_signal 필드 노출 (계획서 2단계 표절검출 고도화) - engine/summarizer.py + POST /v1/summary: TextRank 추출적 요약 + 통합(LLM) 골격 (과제2 ② 스토리 요약/분석) - engine/preference.py + scripts/build_preference_dataset.py: Human Feedback 선호학습(DPO) 데이터 파이프라인 골격 평가 하니스 (정답셋 도착 즉시 측정): - engine/rouge.py + scripts/eval_rouge.py: 요약 ROUGE (지표 No.7) - engine/metadata_eval.py + scripts/eval_metadata_f1.py: 메타 추출 F1 (지표 No.3, KLUE NER 호환) - engine/case_coverage.py + scripts/analyze_case_coverage.py: 39 케이스 갭 분석 → 컴북스 데이터 요청 목록 (정밀도 97% 근거) 문서: - docs/DATA_REQUEST_SPEC.md: 요청 데이터 스펙 + 요약 정답셋/HF 라벨 가이드 - docs/INTEGRATION_INTERFACE.md: 2단계 통합 인터페이스 - docs/SW_COPYRIGHT_REGISTRATION.md: SW 저작권 등록 준비 - docs/PHASE2_PROGRESS.md, docs/CASE_COVERAGE.md 테스트 31 → 67건 전부 통과. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
68 lines
2.5 KiB
Python
68 lines
2.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_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
|