저작권 탭 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:
parent
e522fae7eb
commit
fefb393234
@ -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
|
||||
|
||||
@ -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
205
docs/API_SPEC_BAIKAL.md
Normal 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` 는 현재 로딩된 코퍼스 크기. 실데이터 적재 후 실제 대조 건수로 표시됨.
|
||||
Loading…
Reference in New Issue
Block a user