저작권 탭 UI 매핑(review_summary) + AI 생성 의심도 스텁 추가

나누구 앱 저작권 탭 연동을 위해 detect 응답에 두 블록 추가:
- review_summary: 독창성%/유사도%/유사문장 건수/대조 건수/의심 유무/AI 의심도
  (독창성 환산 캘리브레이션 포함 — 무관한 글은 유사도 0~5%로 눌러 표시)
- ai_generation: AI 생성 의심도 스텁(요청마다 더미, is_stub=true).
  워터마킹+언어특징 분류 정식 구현 전까지 계약 확정용.

바이칼 연동용 API 양식서(docs/API_SPEC_BAIKAL.md) 추가.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
hbyang 2026-08-04 13:15:07 +09:00
parent e522fae7eb
commit fefb393234
3 changed files with 291 additions and 0 deletions

View File

@ -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

View File

@ -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

205
docs/API_SPEC_BAIKAL.md Normal file
View File

@ -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` 는 현재 로딩된 코퍼스 크기. 실데이터 적재 후 실제 대조 건수로 표시됨.