o2o-plagiarism-ai/app/core/config.py
hbyang a530d2139f AI 의심도 백분위 캘리브레이션 (학습·지표 없이 운영)
AI 생성 표본이 없어 정확도를 측정할 수 없는 상태에서, 규칙 기반 휴리스틱을
실제로 쓸 수 있게 만든다. 점수의 의미를 "AI일 확률"에서 "등록 코퍼스의 인간
저작물 대비 문체 이례도"로 재정의해, 라벨 없이도 참인 진술이 되도록 했다.
덤으로 high 배지 비율이 정의상 고정되어 검토 부하가 예측 가능해진다.

- ai_detector: low_cut/high_cut 주입 지원. 둘 다 주어질 때만 적용하고,
  적용되면 model_version 에 +corpus-percentile 을 붙여 컷 출처를 드러낸다.
  is_stub 은 여전히 true — 컷을 맞췄을 뿐 학습된 모델이 아니다.
- calibrate_cuts/percentile/score_text_heuristic 순수 함수 추가.
- scripts/calibrate_ai_detector_cuts.py: 코퍼스 표본의 점수 분포에서 백분위
  컷을 산출하고 .env 두 줄을 출력한다.
- 규칙 점수가 상단에서 clip 되어 p90=p98 이 되면 그 컷은 high 배지를 영원히
  0건으로 만든다. 이 경우 .env 를 출력하지 않고 exit 3 으로 중단하며
  saturated_share/distinct_scores 진단을 남긴다. 유효 표본 100건 미만은 exit 2.
- kiwipiepy 유무가 점수를 바꾸므로 실제 사용 여부를 리포트에 기록하고 경고한다.

기본값은 그대로 비활성(AI_DETECTOR_ALLOW_HEURISTIC=false)이라 켜지 않으면
동작이 바뀌지 않는다. 컷 미설정 시에는 자리표시자임을 note 로 경고한다.

테스트 180건 통과 (신규 13건).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 09:05:48 +09:00

114 lines
4.5 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 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.0.0-pdf-v1.2"
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
persistent_min_exact_span: int = 80
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"
use_llm_extractor: bool = False
use_embedding_similarity: bool = False
# 삼중 유사도 가중치 (실측 기반)
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())
@lru_cache
def get_settings() -> Settings:
return Settings()