o2o-plagiarism-ai/app/core/config.py
hbyang b73d27a850 feat: split author/admin detect views and add case matching KPI basis
월례회의 자료(2026-09-18) p.7 파이프라인의 3단계 「저자 화면에는 케이스 코드를
노출하지 않는다」를 구현하고, 후속 합의에 필요한 문서를 함께 남긴다.

- DetectOptions.audience(admin 기본 / author). author 직렬화에서 case_id,
  case_candidates, tags, legal_risk, is_infringement 을 제외하고 ccl_basis 를
  코드 없는 문장으로 대체한다. 일치 위치와 점수는 유지한다.
- is_infringement 는 필수 bool 로 둔다. run_precision_eval.py 등 소비자가 bool
  로 읽으므로 선택 필드로 두면 None 이 조용히 흘러간다. 제외는 직렬화에서만 한다.
- publication_verdict 필드 추가. 컴북스 코드표 미확보이므로 39건 전부 null 이며
  null 을 출간 허용으로 해석하지 않는다. enum 과 대표값 선정은 코드표 수령 후.
- request_id / taxonomy_version 을 응답에 싣는다. 관리자 확정 로그와 연결된다.
- engine_version 기본값을 2.2.1-cases-v1.3 으로 맞춘다. 직전 값(2.0.1)이 King
  운영값 2.2.0-persistent-cpu 보다 낮아 성적서 대조 시 뒤집혀 보였다.

케이스 정의는 39건(A 27건)을 유지한다. 회의 자료의 40건(A 28건)과 1건 차이가
있으나 아카이빙 DB v2.3 원본을 받기 전까지 추측해 채우지 않는다.

7,786편 운영 재검사는 모집단 불일치(현재 6,343건)로 중단했고 부분 실행은 집계하지
않는다. 별도 평가셋 재측정은 기존 testset_v2 수치(precision 98.4032%)를 그대로
재현했으며 새 독립 시험 결과가 아니다. 상세는 reports/CASE_MATCHING_EVAL_*.json.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 16:16:33 +09:00

130 lines
5.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
from functools import lru_cache
from pathlib import Path
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
# 서버 바인딩
host: str = "0.0.0.0"
port: int = 8000
log_level: str = "info" # debug / info / warning / error
reload: bool = False # 개발용 자동 재시작
root_path: str = "" # 리버스 프록시 sub-path (예: /plagiarism)
# --- 인증 (#1) ---
# api_key 만으로는 "빈 값 = 무인증"이 조용히 성립한다. 운영에서는
# require_api_key=true 로 명시적 fail-closed 를 걸어, 키가 없으면 앱이
# 아예 뜨지 않게 한다.
api_key: str = "" # 설정 시 X-API-Key 필수
require_api_key: bool = False # true 인데 api_key 가 비면 기동 실패
public_health: bool = True # /v1/health 를 인증 없이 공개할지
public_docs: bool = True # /docs, /openapi.json, /redoc 공개 여부
engine_version: str = "o2o-plagiarism-2.2.1-cases-v1.3"
reference_corpus_dir: str = "./data/reference"
taxonomy_dir: str = "./data/taxonomy"
autobiography_patterns_path: str = "./data/autobiography/common_patterns.txt"
corpus_db_path: str = "./data/runtime/corpus.sqlite3"
persistent_index_dir: str = "./data/runtime/index"
use_persistent_index: bool = False
persistent_similarity_threshold: float = 0.65
# 9어절 = 공백 포함 35자. 어절 3~9 스윕과 문서쌍 전수 대조로 정한 값이다.
# 4어절 이하는 임의 조합의 99% 이상이 겹쳐 신호가 되지 못하고, 10~15어절
# 구간부터 겹치는 문서쌍이 고정되어 남는 것이 실제 유사 원고다.
persistent_min_exact_span: int = 35
# 그대로 겹친 구간을 침해 판정의 필수 조건으로 둘지. false 면 유사도나
# 커버리지 단독으로도 채택되어 같은 주제의 글에서 오탐이 발생한다.
require_exact_span_evidence: bool = True
persistent_min_coverage: float = 0.30
# 상위 N개 후보만 SequenceMatcher 정밀 비교 + union coverage 에 참여시킨다.
# 이 값이 곧 요청당 O(질의길이 × 세그먼트길이) 연산의 상한이다 (#3/#5).
persistent_rerank_top_k: int = 20
precedents_path: str = "./data/precedents/precedents.jsonl"
ai_detector_model_path: str = "./data/models/ai_detector.joblib"
ai_detector_allow_heuristic: bool = False
ai_detector_use_pos: bool = True
# 휴리스틱 모드 의심도 구간. scripts/calibrate_ai_detector_cuts.py 가 등록
# 코퍼스(전부 인간 저작)의 점수 분포에서 백분위로 산출한다. 둘 다 설정해야
# 적용되며, 없으면 근거 없는 자리표시자(0.40/0.70)가 쓰인다.
ai_detector_low_cut: float | None = None
ai_detector_high_cut: float | None = None
# PDF VII-4 권장: 정밀도 우선 보수적 임계값
similarity_threshold: float = 0.85
# 임계값이 실데이터로 캘리브레이션되었는지. false 면 API 응답에
# provisional=true 로 노출된다 (#9). 79권 FP 분포 측정 후 true 로 전환.
similarity_threshold_calibrated: bool = False
# KoSimCSE / KoSBERT (PDF VII-3 권장) - 한국어 오픈소스 임베딩
use_kosimcse: bool = False
kosimcse_model: str = "BM-K/KoSimCSE-roberta-multitask"
kosimcse_max_length: int = 512
# OpenAI (옵션 - 자체 모델 없을 때 폴백)
openai_api_key: str = ""
openai_extraction_model: str = "gpt-4o-mini"
openai_embedding_model: str = "text-embedding-3-small"
openai_judge_model: str = "gpt-4o-mini"
use_llm_extractor: bool = False
use_embedding_similarity: bool = False
use_llm_legal_judge: bool = False
llm_judge_timeout_seconds: float = 20.0
llm_judge_max_evidence_chars: int = 4000
# 삼중 유사도 가중치 (실측 기반)
weight_text_sim: float = 0.30
weight_lemma_sim: float = 0.45
weight_char_sim: float = 0.15
weight_motif_sim: float = 0.10
# PDF VII-3 캐스케이딩
use_lsh_filter: bool = True
lsh_threshold: float = 0.3 # 1차 필터는 느슨하게 (재현율 우선)
lsh_top_k: int = 50
# 2단계 고도화: 군집화 기반 부분 표절(요소 교체) 신호 (계획서 p.21)
use_clustering: bool = True
cluster_link_threshold: float = 0.35
# PDF VII-4 자서전 모드
autobiography_mode: bool = True
enable_entity_masking: bool = True
@property
def corpus_path(self) -> Path:
return Path(self.reference_corpus_dir).resolve()
@property
def taxonomy_path(self) -> Path:
return Path(self.taxonomy_dir).resolve()
@property
def corpus_db(self) -> Path:
return Path(self.corpus_db_path).resolve()
@property
def persistent_index_path(self) -> Path:
return Path(self.persistent_index_dir).resolve()
@property
def precedent_path(self) -> Path:
return Path(self.precedents_path).resolve()
@property
def has_openai(self) -> bool:
return bool(self.openai_api_key.strip())
@property
def has_llm_legal_judge(self) -> bool:
return self.use_llm_legal_judge and self.has_openai
@lru_cache
def get_settings() -> Settings:
return Settings()