diff --git a/app/api/schemas.py b/app/api/schemas.py index cbec692..1c56843 100644 --- a/app/api/schemas.py +++ b/app/api/schemas.py @@ -116,6 +116,43 @@ class ExtractedElements(BaseModel): keywords: list[str] = Field(default_factory=list) +class AiGenerationSignal(BaseModel): + """AI 생성 의심도 — 스텁(더미) 응답. + + ⚠️ 현재는 실제 판별 로직이 아니라 요청마다 더미 값을 반환한다. 바이칼 UI 연동 + (낮음/중간/높음 배지)을 위한 API 계약 확정용. 정식 구현은 워터마킹(내부 생성물) + + 한국어 언어특징 분류(외부 유입분)로 대체 예정이며, 그때 is_stub=false 가 된다. + """ + suspicion_level: Literal["low", "medium", "high"] = Field( + ..., description="낮음/중간/높음 — UI 배지용" + ) + score: float = Field(..., ge=0.0, le=1.0, description="0~1 참고 점수") + is_stub: bool = Field(default=True, description="True면 더미 응답(미구현)") + note: str = "더미 응답 — 실제 AI 생성 판별 결과가 아님" + + +class ReviewSummary(BaseModel): + """나누구 '저작권 탭' 화면 직접 매핑용 요약. + + 바이칼이 별도 계산 없이 그대로 표시할 수 있도록, 오투오가 점수 변환(독창성 환산)까지 + 완료해 제공한다. 화면 항목 ↔ 필드 대응: + 독창성 98% ↔ originality_percent + 유사도 2% ↔ similarity_percent + 유사 문장 0건 ↔ similar_sentence_count + 대조 3.5만 건 ↔ compared_count (코퍼스 크기) + 표절 의심 구간 없음 ↔ has_suspicion(false) + AI 생성 의심도 낮음 ↔ ai_suspicion_level (현재 더미) + """ + originality_percent: int = Field(..., ge=0, le=100, description="독창성 % (100 - 유사도)") + similarity_percent: int = Field(..., ge=0, le=100, description="유사도 %") + similar_sentence_count: int = Field(..., ge=0, description="유사 문장(매칭) 건수") + compared_count: int = Field(..., ge=0, description="대조한 원본(코퍼스) 건수") + has_suspicion: bool = Field(..., description="표절 의심 구간 존재 여부") + ai_suspicion_level: Literal["low", "medium", "high"] = Field( + ..., description="AI 생성 의심도(낮음/중간/높음) — 현재 더미" + ) + + class DetectResponse(BaseModel): doc_id: str is_infringement: bool @@ -123,6 +160,8 @@ class DetectResponse(BaseModel): extracted_elements: ExtractedElements matches: list[MatchResult] ccl_basis: str | None = None + review_summary: ReviewSummary | None = None + ai_generation: AiGenerationSignal | None = None autobiography_mode: bool = False candidates_before_filter: int | None = None engine_version: str diff --git a/app/engine/detector.py b/app/engine/detector.py index affa5c4..51afd32 100644 --- a/app/engine/detector.py +++ b/app/engine/detector.py @@ -18,6 +18,7 @@ from datetime import datetime, timezone from app.api.schemas import ( TAG_LABEL_KO, + AiGenerationSignal, DetectOptions, DetectRequest, DetectResponse, @@ -26,6 +27,7 @@ from app.api.schemas import ( InfringementType, MatchResult, PartialPlagiarismSignal, + ReviewSummary, ScoreBreakdown, ) from app.core.config import Settings, get_settings @@ -163,6 +165,18 @@ class PlagiarismDetector: is_infringement = bool(matches) ccl_basis = self._build_ccl_basis(matches) if is_infringement else None + # 저작권 탭 UI 매핑 (나누구 앱). 독창성=100-유사도, AI 의심도는 현재 스텁. + ai_signal = _dummy_ai_generation_signal(text) + sim_pct = _calibrate_similarity(confidence, threshold) + review = ReviewSummary( + originality_percent=100 - sim_pct, + similarity_percent=sim_pct, + similar_sentence_count=len(matches), + compared_count=self.corpus_size, + has_suspicion=is_infringement, + ai_suspicion_level=ai_signal.suspicion_level, + ) + return DetectResponse( doc_id=doc_id, is_infringement=is_infringement, @@ -170,6 +184,8 @@ class PlagiarismDetector: extracted_elements=elements, matches=matches, ccl_basis=ccl_basis, + review_summary=review, + ai_generation=ai_signal, autobiography_mode=autobio_mode, candidates_before_filter=candidates_count, engine_version=self.settings.engine_version, @@ -297,6 +313,37 @@ class PlagiarismDetector: ) +def _calibrate_similarity(raw: float, threshold: float) -> int: + """결합 유사도(0~1) → 사용자 표시용 '유사도 %' 캘리브레이션. + + 임베딩 성분 때문에 무관한 글도 raw 0.5 전후가 나오므로, 그대로 %로 쓰면 + 깨끗한 글이 '유사도 50%'로 보인다. 아래 구간 변환으로 무관한 글은 0~5%, + 임계 초과(실제 표절)만 40% 이상으로 눌러준다. + """ + floor = 0.55 # 무관한 글의 전형적 결합 유사도 상한 + if raw <= floor: + disp = (raw / floor) * 5.0 if floor else 0.0 + elif raw <= threshold: + disp = 5.0 + (raw - floor) / max(1e-6, threshold - floor) * 35.0 + else: + disp = 40.0 + (raw - threshold) / max(1e-6, 1.0 - threshold) * 60.0 + return max(0, min(100, round(disp))) + + +def _dummy_ai_generation_signal(text: str) -> AiGenerationSignal: + """AI 생성 의심도 스텁 — 실제 판별이 아니라 텍스트 기반 결정적 더미 값. + + 바이칼 UI(낮음/중간/높음 배지) 연동용 계약 확정 목적. 정식 구현(워터마킹+ + 언어특징 분류) 전까지 is_stub=True 로 반환한다. + """ + h = sum(ord(c) for c in text[:300]) % 100 + if h < 70: + return AiGenerationSignal(suspicion_level="low", score=round(0.05 + h / 500, 3)) + if h < 90: + return AiGenerationSignal(suspicion_level="medium", score=round(0.45 + (h - 70) / 200, 3)) + return AiGenerationSignal(suspicion_level="high", score=round(0.72 + (h - 90) / 200, 3)) + + def _classify_legacy(hit: SimilarityHit) -> InfringementType: """후방 호환 - 단일 enum 분류 (UI/기존 통합 코드용).""" elem = hit.element_sim diff --git a/docs/API_SPEC_BAIKAL.md b/docs/API_SPEC_BAIKAL.md new file mode 100644 index 0000000..1e98c5e --- /dev/null +++ b/docs/API_SPEC_BAIKAL.md @@ -0,0 +1,205 @@ +# 저작권 탐지 API 양식서 (오투오 → 바이칼) + +> 나누구 앱 **저작권 탭** 연동용 API 계약. 본문 입력 → 표절 탐지 결과 + 저작권 탭 +> 표시 항목을 반환한다. 본 문서 기준으로 UI를 연동하면 된다. +> 엔진: `o2o-plagiarism-api` · 문의: 오투오 양형배 + +--- + +## 0. 기본 정보 + +| 항목 | 값 | +|---|---| +| Base URL (개발/통합 테스트) | `https://plagiarism.o2o.kr` | +| 프로토콜 | HTTPS, `Content-Type: application/json` (UTF-8) | +| 인증 | 개발 단계 없음 → **운영 전 API 키/토큰 추가 예정** | +| Swagger(자동 명세) | `{BASE}/docs` — 실시간 스키마 확인 가능 | +| 실서비스 배포 | 개인정보(자서전) 보호를 위해 **Docker 온프레미스 납품** 예정 (실사용자 본문은 오투오 서버로 전송 안 함) | + +--- + +## 1. 저작권 탭 화면 ↔ API 필드 매핑 (핵심) + +나누구 저작권 탭의 각 표시 항목은 아래 필드에서 그대로 읽으면 된다. **별도 계산 불필요** +— 독창성 환산(점수 변환)까지 오투오가 완료해 `review_summary`로 제공한다. + +| 화면 표시 | 예시 | API 필드 | +|---|---|---| +| 독창성 **98%** | 98 | `review_summary.originality_percent` | +| 저작권 · 유사도 **2%** | 2 | `review_summary.similarity_percent` | +| 유사 문장 **0건** | 0 | `review_summary.similar_sentence_count` | +| 나누구 에피소드 **3.5만 건**과 대조 | 35000 | `review_summary.compared_count` | +| 표절 의심 구간 **없음** | false | `review_summary.has_suspicion` | +| AI 생성 의심도 **낮음** | low | `review_summary.ai_suspicion_level` | + +> `ai_suspicion_level` 값 매핑: `low`=낮음, `medium`=중간, `high`=높음 +> **⚠️ AI 생성 의심도는 현재 더미(스텁) 값입니다.** UI 연동 계약 확정용이며, 정식 판별 +> 로직(워터마킹+언어특징 분류) 적용 전까지 `ai_generation.is_stub=true` 로 반환됩니다. +> UI는 지금 그대로 붙여두면 되고, 정식 구현 시 값만 실제로 바뀝니다. + +--- + +## 2. 표절 탐지 — `POST /v1/plagiarism/detect` + +본문 1건을 검사한다. + +### 요청 + +```json +{ + "doc_id": "episode-001", + "text": "창밖으로 보이는 숲은 오늘따라 유난히 푸르게 보였다. ...", + "metadata": { "title": "에피소드 1", "author": "홍길동", "genre": "자서전" }, + "options": { + "return_evidence": true, + "threshold": null, + "top_k": 5, + "autobiography_mode": null + } +} +``` + +| 필드 | 타입 | 필수 | 설명 | +|---|---|---|---| +| `doc_id` | string | ✅ | 호출측 문서 식별자 (응답에 그대로 반환) | +| `text` | string | ✅ | 검사할 본문 (1자 이상) | +| `metadata` | object | – | `title`/`author`/`genre`/`publisher`/`publication_year` (모두 선택) | +| `options.return_evidence` | bool | – | 일치 구간 하이라이트 반환 (기본 true) | +| `options.threshold` | float\|null | – | 판정 임계값. null이면 서버 기본(0.85) | +| `options.top_k` | int | – | 최대 매칭 수 (기본 5) | +| `options.autobiography_mode` | bool\|null | – | 자서전 특화 전처리. null이면 서버 설정 | + +### 응답 (저작권 탭에 표시되는 "깨끗한 글" 예시) + +```json +{ + "doc_id": "episode-001", + "is_infringement": false, + "confidence": 0.32, + "review_summary": { + "originality_percent": 98, + "similarity_percent": 2, + "similar_sentence_count": 0, + "compared_count": 35000, + "has_suspicion": false, + "ai_suspicion_level": "low" + }, + "ai_generation": { + "suspicion_level": "low", + "score": 0.06, + "is_stub": true, + "note": "더미 응답 — 실제 AI 생성 판별 결과가 아님" + }, + "matches": [], + "extracted_elements": { + "characters": [], "motifs": [], "genre": "자서전", "keywords": ["정원", "숲"] + }, + "ccl_basis": null, + "autobiography_mode": true, + "candidates_before_filter": 0, + "engine_version": "o2o-plagiarism-2.1.0-kosimcse", + "analyzed_at": "2026-08-04T09:30:00Z" +} +``` + +### 응답 (표절이 탐지된 경우 — matches가 채워짐) + +```json +{ + "doc_id": "episode-002", + "is_infringement": true, + "confidence": 0.88, + "review_summary": { + "originality_percent": 45, + "similarity_percent": 55, + "similar_sentence_count": 1, + "compared_count": 35000, + "has_suspicion": true, + "ai_suspicion_level": "low" + }, + "ai_generation": { "suspicion_level": "low", "score": 0.06, "is_stub": true, "note": "더미 응답 — 실제 AI 생성 판별 결과가 아님" }, + "matches": [ + { + "source_doc": "auto-0003", + "source_title": "○○○ 자서전", + "similarity": 0.88, + "tags": [ + { "tag": "reproduction", "role": "primary", "label_ko": "복제권" }, + { "tag": "citation_missing", "role": "primary", "label_ko": "인용 표시 누락" } + ], + "case_id": "A6", + "case_title": "유명인 자서전 일부 베끼기", + "infringement_type": "copy", + "evidence_spans": [ { "start": 12, "end": 48, "matched": "일치한 본문 구간 ..." } ], + "score_breakdown": { "text_sim": 0.86, "lemma_sim": 0.94, "character_sim": 0.8, "motif_sim": 0.7, "lsh_jaccard": 0.62 }, + "partial_signal": { + "cluster_id": 2, "verdict": "element_swap_plagiarism", "signature_score": 0.74, + "per_element": { "lemmas": 0.92, "keywords": 0.88, "characters": 0.0, "motifs": 0.1 }, + "retained_elements": ["lemmas", "keywords"], "changed_elements": ["characters", "motifs"] + } + } + ], + "ccl_basis": "'○○○ 자서전'와 결합 유사도 88%로 매칭. 주 침해 태그: 복제권, 인용 표시 누락. 추정 케이스 A6 ...", + "autobiography_mode": true, + "candidates_before_filter": 4, + "engine_version": "o2o-plagiarism-2.1.0-kosimcse", + "analyzed_at": "2026-08-04T09:31:00Z" +} +``` + +### 응답 필드 상세 + +| 필드 | 타입 | 설명 | +|---|---|---| +| `is_infringement` | bool | 표절 판정 여부 (= `review_summary.has_suspicion`) | +| `confidence` | float | 최상위 매칭 결합 유사도 원값(0~1). **화면 표시는 `review_summary` 사용 권장** | +| `review_summary` | object | **저작권 탭 UI 직접 매핑** (§1 표 참조) | +| `ai_generation` | object | AI 생성 의심도 상세. `is_stub=true`면 더미 | +| `matches[]` | array | 매칭된 원본별 상세 (표절 시). 태그·케이스·근거 구간 포함 | +| `matches[].tags[]` | array | 법령 태그. `role`: `primary`(주)/`secondary`(보조), `label_ko` 한글 표기 | +| `matches[].case_id` | string | 39종 침해 케이스 ID (예: A6) | +| `matches[].evidence_spans[]` | array | 본문 내 일치 구간 `{start, end, matched}` — 하이라이트용 | +| `matches[].partial_signal` | object | 군집화 기반 부분 표절(인물만 교체 등) 분해 — 침해요소 DB 적재용 | +| `ccl_basis` | string\|null | 사람이 읽는 판정 근거 문장 | +| `engine_version` | string | 엔진 버전 | + +--- + +## 3. 부가 엔드포인트 + +| Method | Path | 용도 | +|---|---|---| +| POST | `/v1/plagiarism/batch` | 배치 검사(≤500건). `202` + `job_id` 반환 → 아래 상태 조회 | +| GET | `/v1/plagiarism/batch/{job_id}` | 배치 상태·결과 조회 | +| POST | `/v1/summary` | 스토리 요약 (과제2 ②) | +| GET | `/v1/taxonomy` | 10종 법령 태그 + 39 케이스 정의 (동일 라벨 공유용) | +| GET | `/v1/health` | 엔진 상태·코퍼스 크기·버전 | + +--- + +## 4. 에러 포맷 + +FastAPI 표준. HTTP 상태코드 + `detail`. + +```json +{ "detail": "에러 메시지" } +``` + +| 코드 | 상황 | +|---|---| +| 400 | 잘못된 요청(빈 본문 등) | +| 404 | 배치 job_id 없음 | +| 422 | 스키마 검증 실패 | +| 503 | 분류체계 미로딩 | + +--- + +## 5. 연동 시 주의 + +1. **AI 생성 의심도는 현재 더미** — `is_stub` 로 판별 후, UI에는 표시하되 "참고용"으로 둘 것. + 출판 승인/거절 게이트로 쓰지 말 것(오탐 리스크). +2. **화면 값은 `review_summary` 사용** — `confidence`(원값)는 임베딩 성분 때문에 무관한 글도 + 높게 나오므로 직접 표시 금지. 독창성 환산은 오투오가 `review_summary`에서 완료해 제공. +3. **개인정보** — 실사용자 자서전 본문은 개인정보. 실서비스는 온프레미스(Docker) 납품으로 + 전환 예정. 개발 단계 REST 호출 시에도 테스트 데이터 사용 권장. +4. `compared_count` 는 현재 로딩된 코퍼스 크기. 실데이터 적재 후 실제 대조 건수로 표시됨.