o2o-plagiarism-ai/docs/API_GUIDE_BAIKAL.md

395 lines
20 KiB
Markdown
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.

# 저작권 탐지 API 상세 연동 가이드 (오투오 → 바이칼)
> 나누구 앱 연동용. 각 엔드포인트 사용법 · 응답 변수 의미 · **저작권 탭 화면을 그리기
> 위한 호출 순서**를 정리한다. 실시간 스키마: `https://plagiarism.o2o.kr/docs`
> 엔진: `o2o-plagiarism-2.1.0-kosimcse`
> 최종 갱신: `2026-08-18` · 판례 545건 및 GPT 판례 판단 연동 반영
---
## 0. 공통
| 항목 | 값 |
|---|---|
| Base URL | `https://plagiarism.o2o.kr` |
| 공통 prefix | 모든 API는 `/v1` 으로 시작 |
| 요청/응답 | JSON, UTF-8, `Content-Type: application/json` |
| 인증 | 개발 기본값은 무인증. 운영에서 설정된 경우 `X-API-Key` 헤더 필수 (`/v1/health` 공개 여부는 서버 설정) |
| 시간 형식 | ISO 8601 UTC (예: `2026-08-04T04:37:00Z`) |
### 엔드포인트 한눈에
| # | Method | Path | 용도 | 화면 연동 |
|---|---|---|---|---|
| 1 | GET | `/v1/health` | 엔진 상태·코퍼스 크기 확인 | 앱 기동 시 1회 |
| 2 | POST | `/v1/plagiarism/detect` | **본문 1건 표절 탐지** | **저작권 탭 핵심** |
| 3 | POST | `/v1/plagiarism/batch` | 배치 탐지(≤500) 등록 | 대량 검사용 |
| 4 | GET | `/v1/plagiarism/batch/{job_id}` | 배치 결과 조회 | 대량 검사용 |
| 5 | POST | `/v1/summary` | 스토리 요약 | 요약 기능용 |
| 6 | GET | `/v1/taxonomy` | 법령 태그·케이스 정의 | 라벨 캐시용 |
| 7 | GET | `/v1/corpus` | 코퍼스 목록 | 운영/관리 |
| 8 | POST | `/v1/corpus` | 코퍼스 등록(JSON) | 운영/관리 |
| 9 | POST | `/v1/corpus/file` | 코퍼스 등록(.txt 업로드) | 운영/관리 |
| 10 | DELETE | `/v1/corpus/{doc_id}` | 코퍼스 삭제 | 운영/관리 |
---
## 1. 저작권 탭을 그리기 위한 호출 순서 ★
가장 중요한 부분. 저작권 탭 화면 하나를 채우는 데 필요한 호출 흐름이다.
```
[앱 최초 기동 / 세션 시작 시 — 1회]
① GET /v1/health → 엔진 정상·corpus_size 확인 (실패면 탭 비활성)
② GET /v1/taxonomy → 태그/케이스 한글 라벨 캐시 (표절 상세 표시용, 선택)
[사용자가 에피소드 작성 후 '저작권' 탭을 열 때 — 매 검사]
③ POST /v1/plagiarism/detect { doc_id, text: 에피소드 본문 }
└ 응답 1건으로 저작권 탭 전체를 렌더링:
• review_summary → 상단 요약(독창성/유사도/유사문장/AI 의심도)
• matches[] → 표절 의심 시 상세 목록
• matches[].evidence_spans → 본문 내 일치 구간 하이라이트
• matches[].tags / case_id → 침해 유형 배지 (②의 라벨과 결합)
• ccl_basis → 사람이 읽는 판정 근거 문장
• legal_risk → 판례 Top 5 + GPT/규칙 기반 법적 검토 보조
• score_semantics → 점수·임계값의 의미와 잠정 여부
```
**핵심: 탭 렌더링에 필요한 호출은 `detect` 단 1번**이다. `health`/`taxonomy`는
세션당 1회 캐시하면 된다. 화면 상단 숫자는 전부 `review_summary`에서 나오므로,
바이칼 쪽에서 별도 계산할 것은 없다.
### 화면 요소 ↔ 응답 필드 매핑
| 저작권 탭 화면 | 값 예시 | 응답 경로 |
|---|---|---|
| 독창성 98% | 98 | `review_summary.originality_percent` |
| 저작권 · 유사도 2% | 2 | `review_summary.similarity_percent` |
| "…3.5만 건과 대조한 결과 표절 의심 구간이 없습니다" | 35000 / false | `review_summary.compared_count`, `review_summary.has_suspicion` |
| 유사 문장 0건 | 0 | `review_summary.similar_sentence_count` |
| AI 생성 의심도 | unknown | `review_summary.ai_suspicion_level` (`low/medium/high/unknown`) |
| (표절 시) 일치 구간 하이라이트 | start~end | `matches[].evidence_spans[]` |
| (표절 시) 침해 유형 배지 | 복제권 | `matches[].tags[].label_ko` + `case_id` |
| 판례 기반 검토 의견 | 판단 필요 | `legal_risk.llm_verdict`, `legal_risk.supporting_reasons[]` |
| 관련 판례 | 사건번호 | `legal_risk.llm_matched_precedent_ids[]` 또는 `precedent_ids[]` |
---
## 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 },
"legal_context": {
"work_type": "literary",
"access_evidence": null,
"protected_expression_reviewed": false,
"rights_verified": false
}
}
```
| 필드 | 타입 | 필수 | 의미 |
|---|---|---|---|
| `doc_id` | string | ✅ | 호출측 문서 식별자. 응답에 그대로 반환(요청-응답 짝 맞춤용) |
| `text` | string | ✅ | 검사할 본문 (1자 이상) |
| `metadata.title` | string | – | 작품 제목 (선택) |
| `metadata.author` | string | – | 저자 (선택) |
| `metadata.genre` | string | – | 장르 (선택) |
| `metadata.publisher` | string | – | 출판사 (선택) |
| `metadata.publication_year` | int | – | 출판연도 (선택) |
| `options.return_evidence` | bool | – | 일치 구간(`evidence_spans`) 반환 여부. 기본 `true` |
| `options.threshold` | float 0~1 \| null | – | 표절 판정 임계값. `null`이면 서버 기본(0.85) |
| `options.top_k` | int | – | 반환할 최대 매칭 수. 기본 5 |
| `options.autobiography_mode` | bool \| null | – | 자서전 특화 전처리. `null`이면 서버 설정 |
| `legal_context.work_type` | string | – | 판례 검색용 저작물 유형. 기본 `literary` |
| `legal_context.access_evidence` | bool \| null | – | 원저작물 접근·의거 가능성 확인 여부. 미확인은 `null` |
| `legal_context.protected_expression_reviewed` | bool | – | 보호되는 창작적 표현인지 사람이 검토했는지 |
| `legal_context.rights_verified` | bool | – | 권리 귀속·이용허락·인용 요건 확인 여부 |
> `legal_context`는 검토자가 확인한 사실만 전달한다. 서버와 GPT는 원고만 보고
> 접근 가능성, 권리 귀속 또는 이용허락 여부를 임의로 추론하지 않는다.
### 응답 — 전체 필드 의미
```json
{
"doc_id": "episode-001",
"is_infringement": false,
"confidence": 0.14,
"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": "unknown", "score": null, "available": false, "is_stub": false, "model_version": "unavailable", "note": "학습된 모델이 없어 채점하지 않음" },
"matches": [],
"extracted_elements": { "characters": [], "motifs": ["비밀의 정원"], "genre": null, "keywords": ["숲","전설"] },
"ccl_basis": null,
"has_similarity_match": false,
"corpus_scope_note": "현재 등록된 원천 문서와 검색 세그먼트만 대조했습니다. 미매칭은 비침해 확정이 아닙니다.",
"legal_risk": {
"status": "no_registered_corpus_match",
"risk_level": "low",
"similarity_evidence": "검색 유사도 0.000, 질의 커버리지 0.000, 최장 연속 일치 0자",
"protected_expression": "not_reviewed",
"access_evidence": "not_provided",
"missing_factors": ["보호되는 창작적 표현인지에 대한 사람 검토", "원저작물 접근·의거 가능성", "저작권 귀속·이용허락·인용 요건"],
"precedent_ids": [],
"judgment_method": "rule_based",
"llm_verdict": null,
"llm_confidence": null,
"llm_review_required": null,
"llm_matched_precedent_ids": [],
"supporting_reasons": [],
"counter_reasons": [],
"judge_model": null,
"judge_prompt_version": null,
"judge_note": null,
"judgment_summary": "등록 코퍼스에서 일치 증거가 확인되지 않아 판례 기반 침해 의심을 제시하지 않습니다. 다만 미매칭은 비침해 확정이 아닙니다.",
"disclaimer": "이 결과는 등록 코퍼스와 판례에 기반한 검토 우선순위이며 법률상 침해 확정이 아닙니다."
},
"score_semantics": {
"combined_score": 0.14,
"score_kind": "lexical_lemma_blend",
"threshold_used": 0.85,
"threshold_source": "server_default",
"threshold_calibrated": false,
"provisional": true,
"union_coverage": 0.0,
"covered_chars": 0,
"query_chars": 42,
"evidence_truncated": false
},
"autobiography_mode": true,
"candidates_before_filter": 3,
"engine_version": "o2o-plagiarism-2.1.0-kosimcse",
"analyzed_at": "2026-08-04T04:37:00Z"
}
```
**최상위 필드**
| 필드 | 타입 | 의미 | 화면 사용 |
|---|---|---|---|
| `doc_id` | string | 요청의 doc_id 그대로 | 응답 매칭 |
| `is_infringement` | bool | 후방호환 필드. 법적 침해 확정이 아니라 임계 초과 매칭 존재 여부 | — |
| `confidence` | float 0~1 | 최상위 매칭의 **결합 유사도 원값**. ⚠️ 임베딩 성분 때문에 무관한 글도 높게 나옴 → **화면 표시는 `review_summary` 사용, `confidence` 직접 표시 금지** | ✗ |
| `review_summary` | object | **저작권 탭 UI 요약** (아래 상세) | ★ |
| `ai_generation` | object | AI 생성 의심도 상세 (아래 상세) | 참고 |
| `matches` | array | 매칭된 원본별 상세. 표절 아니면 `[]` | 표절 상세 |
| `extracted_elements` | object | 본문에서 추출한 구성요소 `characters/motifs/genre/keywords` | 부가 |
| `ccl_basis` | string\|null | 사람이 읽는 판정 근거 문장. 표절 아니면 `null` | 상세 문구 |
| `has_similarity_match` | bool | 등록 코퍼스에서 채택된 매칭 존재 여부 | 분기 |
| `corpus_scope_note` | string | 미매칭의 한계를 설명하는 안내 문구 | 안내 |
| `legal_risk` | object | 등록 판례와 선택적 GPT Judge 기반의 **법적 검토 보조** | 검토 카드 |
| `score_semantics` | object | 점수 종류·임계값·커버리지·잠정 여부 | 감사/상세 |
| `autobiography_mode` | bool | 자서전 전처리 적용 여부 | — |
| `candidates_before_filter` | int\|null | LSH 1차 필터 통과 후보 수(내부 지표) | — |
| `engine_version` | string | 엔진 버전 | 로깅 |
| `analyzed_at` | datetime | 분석 시각(UTC) | 로깅 |
**`review_summary` (저작권 탭 요약 — 그대로 표시)**
| 필드 | 타입 | 의미 |
|---|---|---|
| `originality_percent` | int 0~100 | **독창성 %** = 100 − 유사도. 점수 변환(캘리브레이션)을 오투오가 완료해 제공 |
| `similarity_percent` | int 0~100 | **유사도 %** (무관한 글은 0~5%로 눌러 표시) |
| `similar_sentence_count` | int | **유사 문장 건수** = 임계 초과 매칭 수 |
| `compared_count` | int | **대조한 원본(코퍼스) 건수** |
| `has_suspicion` | bool | **표절 의심 구간 존재 여부** |
| `ai_suspicion_level` | `low/medium/high/unknown` | **AI 생성 의심도**. 미학습/채점 불가는 `unknown` |
**`ai_generation` (AI 생성 의심도 상세)**
| 필드 | 타입 | 의미 |
|---|---|---|
| `suspicion_level` | `low/medium/high` | 의심도 등급 |
| `score` | float 0~1 | 참고 점수 |
| `available` | bool | 학습 모델 또는 명시적으로 활성화한 baseline으로 채점했는지 |
| `is_stub` | bool | `true`면 미검증 휴리스틱 baseline. 학습 모델은 `false` |
| `model_version` | string | 학습 아티팩트/특징 버전 추적 |
| `note` | string | 안내 문구 |
> 학습 모델이 없으면 점수를 임의 생성하지 않고 `available=false`, `score=null`,
> `suspicion_level=unknown`을 반환한다. 학습 후에도 **출판 승인 게이트나 저자 제재의
> 단독 근거로 쓰지 말 것**(사람 검토 우선순위용).
**`matches[]` (표절 의심 시 채워짐)**
| 필드 | 타입 | 의미 |
|---|---|---|
| `source_doc` | string | 매칭된 원본 doc_id |
| `source_title` | string\|null | 매칭된 원본 제목 |
| `similarity` | float 0~1 | 해당 원본과의 결합 유사도 |
| `tags[]` | array | 법령 태그. `{ tag, role, label_ko }` |
| `tags[].tag` | string | 태그 코드 (예: `reproduction`) |
| `tags[].role` | `primary/secondary` | 주 침해 / 보조 |
| `tags[].label_ko` | string | 한글 표기 (예: `복제권`) — 배지에 그대로 사용 |
| `case_id` | string\|null | 39종 침해 케이스 ID (예: `A6`) |
| `case_title` | string\|null | 케이스 명칭 |
| `infringement_type` | string | 후방호환 단일 분류(`copy/transform/plot/character/unknown`) |
| `evidence_spans[]` | array | 본문 내 일치 구간 `{ start, end, matched }` — **하이라이트용** |
| `score_breakdown` | object | 유사도 분해 `text_sim/lemma_sim/character_sim/motif_sim/lsh_jaccard` |
| `partial_signal` | object\|null | 군집화 부분 표절(인물만 교체 등) 분해 — 침해요소 DB 적재용 |
### `legal_risk` (판례 기반 LLM-as-a-Judge 검토 보조)
등록된 국내 저작권 판례 545건 중 저작물 유형과 법적 태그가 가까운 Top 5를 먼저
선별한다. 운영에서 `USE_LLM_LEGAL_JUDGE=true`이고 OpenAI API 키가 설정되어 있으면,
GPT가 탐지 증거와 이 Top 5만 비교해 구조화된 의견을 반환한다. 전체 판례를 매 요청에
전송하지 않는다.
| 필드 | 타입 | 의미 |
|---|---|---|
| `status` | `review_required/no_registered_corpus_match/insufficient_precedent_data` | 사람 검토 상태. 법적 확정 판정이 아님 |
| `risk_level` | `low/medium/high/null` | 규칙 기반 검토 우선순위 |
| `precedent_ids[]` | string[] | 규칙 엔진이 검색한 판례 Top 5 사건번호 |
| `judgment_method` | `llm/rule_based/rule_fallback` | GPT 성공, 기능 비활성, GPT 실패 후 폴백 구분 |
| `llm_verdict` | `likely/unlikely/insufficient_evidence/null` | GPT의 비교 의견. 침해 확정값이 아님 |
| `llm_confidence` | float 0~1\|null | GPT 자기평가 값. 통계적 침해 확률이 아님 |
| `llm_review_required` | bool\|null | GPT 결과는 항상 `true`; 모델이 사람 검토를 해제할 수 없음 |
| `llm_matched_precedent_ids[]` | string[] | GPT가 실제 근거로 선택한 사건번호. Top 5 밖의 ID는 서버가 거부 |
| `supporting_reasons[]` | string[] | 의심 의견을 지지하는 근거 |
| `counter_reasons[]` | string[] | 반대 근거·판단을 약화하는 사실 |
| `missing_factors[]` | string[] | 보호 표현, 의거관계, 권리·허락 등 추가 확인사항 |
| `judge_model` | string\|null | 사용한 GPT 모델명 |
| `judge_prompt_version` | string\|null | 감사·재현용 프롬프트 버전 |
| `judge_note` | string\|null | 폴백 또는 법적 한계 안내 |
| `judgment_summary` | string | 검증된 판례 사건번호와 핵심 사유를 결합한 사용자 표시용 의견 |
`matches[].case_id`는 39종 내부 침해유형 ID이고, `legal_risk.*precedent_ids`는 실제
판례 사건번호다. 두 필드를 혼용하지 않는다. UI에서는 `llm_verdict`만 단독 표시하지
말고 `judgment_summary`를 첫 문장으로 표시한 뒤 찬반 근거, 누락 요소, 판례 사건번호
및 면책 문구를 함께 보여준다. 모델명이나 “GPT 판례 비교” 같은 구현 용어는 사용자용
판정 제목으로 표시하지 않는다.
### `score_semantics` (점수 해석)
- `combined_score`는 검색 랭킹 점수이며 침해 확률이 아니다.
- `provisional=true`이면 임계값이 실데이터로 확정되지 않은 잠정값이다.
- `union_coverage`는 질의 전체에서 비중복 일치 구간이 차지하는 비율이다.
- `evidence_truncated=true`이면 CPU 상한 때문에 일부 후보의 정밀 증거 계산이 생략됐다.
---
## 3. `GET /v1/health` — 엔진 상태
앱 기동 시 호출해 엔진 준비·코퍼스 규모를 확인한다.
**응답**
| 필드 | 타입 | 의미 |
|---|---|---|
| `status` | `"ok"` | 정상 |
| `engine_version` | string | 엔진 버전 |
| `corpus_size` | int | 로딩된 대조 원본 수 (`review_summary.compared_count`와 동일) |
| `taxonomy_version` | string\|null | 분류체계 버전 |
| `autobiography_mode` | bool | 자서전 모드 on/off |
| `corpus_documents` | int | 등록된 원천 문서 수 |
| `index_backend` | string | 현재 검색 인덱스 구현 |
| `ai_model_ready` | bool | 학습된 AI 생성 탐지 모델 준비 여부 |
| `precedent_count` | int | 로딩된 판례 수. 현재 운영 데이터는 545건 |
---
## 4. `GET /v1/taxonomy` — 법령 태그·케이스 정의
10종 태그와 39종 케이스 정의를 반환. 세션당 1회 받아 캐시하면, `detect` 응답의
`tag`/`case_id`를 화면에 설명과 함께 표시할 수 있다.
**응답**: `meta_tags_version`, `cases_version`, `meta_tags[]`, `cases[]`
- `meta_tags[]` 각 항목: `id`, `label_ko`, `category`, `law_ref`, `scope`, `description`
- `cases[]` 각 항목: `case_id`, `title`, `subgroup`, `actor`, `primary_tags[]`, `secondary_tags[]`, `detectable_internal`, `high_risk`, `note`
---
## 5. `POST /v1/plagiarism/batch` + `GET /v1/plagiarism/batch/{job_id}` — 배치
여러 건을 한 번에 검사(비동기). 대량 점검용.
**등록 요청** `POST /v1/plagiarism/batch`
```json
{ "items": [ { "doc_id": "e1", "text": "..." }, { "doc_id": "e2", "text": "..." } ],
"options": { "threshold": null } }
```
- `items`: 1~500건. 각 `{ doc_id, text, metadata? }`
- 응답(202): `{ job_id, status, total, created_at }`
**결과 조회** `GET /v1/plagiarism/batch/{job_id}`
| 필드 | 의미 |
|---|---|
| `status` | `queued/running/completed/failed` |
| `total` / `processed` | 전체 / 처리 완료 건수 (진행률) |
| `results` | `status=completed`일 때만. `DetectResponse` 배열(§2와 동일 구조) |
| `error` | 실패 시 사유 |
> 폴링: 등록 → `job_id`로 `completed` 될 때까지 조회 → `results` 사용.
---
## 6. `POST /v1/summary` — 스토리 요약
**요청**: `{ "text": "...", "ratio": 0.3, "max_sentences": null, "use_abstractive": true }`
- `ratio`: 요약 길이 비율(입력 대비). `use_abstractive`: LLM 결합(키 없으면 추출 요약 폴백)
**응답**: `extractive`(추출 요약), `abstractive`(추상 요약\|null), `final`(최종), `mode`(`extractive/hybrid`), `num_sentences_in/out`
---
## 7. 코퍼스 관리 (운영/관리용)
대조 원본을 등록·삭제. 업로드/삭제 시 인덱스 자동 재빌드.
| API | 설명 |
|---|---|
| `GET /v1/corpus` | 목록. `{ total, docs[{doc_id,title,size_bytes,filename}] }` |
| `POST /v1/corpus` | JSON 등록 `{ doc_id?(자동), title, text }` → `201` |
| `POST /v1/corpus/file` | multipart `.txt` 업로드 (`title`, `doc_id?`, `file`) → `201` |
| `DELETE /v1/corpus/{doc_id}` | 삭제 → `204` |
등록 응답(`201`): `{ doc_id, title, size_bytes, corpus_size_after, rebuilt }`
---
## 8. 에러 포맷
FastAPI 표준. `{ "detail": "메시지" }`
| 코드 | 상황 |
|---|---|
| 400 | 잘못된 요청(빈 본문/비 UTF-8 파일 등) |
| 404 | 배치 job_id 없음 / 코퍼스 doc_id 없음 |
| 409 | 코퍼스 doc_id 중복 |
| 422 | 스키마 검증 실패(필드 타입·범위) |
| 503 | 분류체계 미로딩 |
---
## 9. 빠른 시작 (curl)
```bash
# 1) 엔진 상태
curl https://plagiarism.o2o.kr/v1/health
# 2) 저작권 탭용 탐지 — 이 응답 1건으로 탭 렌더링
curl -X POST https://plagiarism.o2o.kr/v1/plagiarism/detect \
-H "Content-Type: application/json" \
-H "X-API-Key: 운영에서 발급된 키" \
-d '{"doc_id":"episode-001","text":"검사할 본문 텍스트...","legal_context":{"work_type":"literary","access_evidence":null,"protected_expression_reviewed":false,"rights_verified":false}}'
```
> 개발 서버가 무인증 설정이면 `X-API-Key` 헤더를 생략할 수 있다. 운영 서버의 인증
> 적용 여부와 발급된 키를 확인한 뒤 연동한다.