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>
114 lines
4.5 KiB
Python
114 lines
4.5 KiB
Python
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()
|