POSA_Copyrighter/docs/operations/copyrighter-operation-worklist.md
유창욱 3f7b3a9cf2 chore: initial commit of copyrighter (rights_filter)
Image rights / copyright detection system: SQLite store, HTTP app,
search integrations (Naver, Google Custom Search, Google Cloud Vision
web detection), image analysis (fingerprints, face/person detection,
evidence enrichment, risk scoring), an admin/review layer, governance
and retention policies, batch jobs, and a browser-based operator GUI.

This baseline incorporates a full code-review remediation pass
(46 fixes; 358 tests passing). Highlights:

CRITICAL
- Prevent evidence cascade-delete during the schema-constraint
  migration by disabling FK enforcement around the table rebuild.

Security
- Sandbox served media (neutralize stored XSS from uploaded/collected
  SVGs) via CSP + nosniff on the untrusted media routes.
- Strip embedded EXIF/GPS from external image derivatives before they
  are sent to third-party APIs.
- Return a clean 404 (not an uncaught StopIteration) for PATCH on an
  unknown provider.

Correctness
- LLM-summary failures no longer add +30 to the risk score.
- Decode only explicit JS escapes so Korean image URLs are not mangled.
- Consume search quota only after a successful request.
- Naver/Google adapters map responses inside the failure boundary, so a
  malformed response degrades to evidence instead of crashing enrichment.
- Domain-aware provider attribution; face-box IoU de-duplication; count
  searches (not result items); per-box crop isolation; clamp evidence
  confidence and Google CSE num; real submittedEpoch; and more.

Robustness
- Offline LLM connect fast-fails (short connect timeout) so seed/reload
  requests are not stalled; full read timeout preserved for generation.
- Malformed numeric env vars fall back to defaults instead of crashing
  startup.

Performance
- Per-submission evidence reads (no full-table scan per rescore),
  audit-log LIMIT, lazy active-store lookup, hoisted timestamps.

Tests
- ~24 regression tests added pinning the above fixes.

Runtime data (data/, outputs/, *.sqlite3, *.log), secrets (.env), and
node_modules are gitignored.
2026-06-09 09:50:31 +09:00

251 lines
15 KiB
Markdown

# Copyrighter 운영 연결 상태
기준 포트는 `9500`이다.
## 이번에 연결된 항목
- 9500 API 서버 골격 생성 완료
- 정적 운영자 콘솔을 9500 서버에서 함께 제공
- `web/operator-gui/app.js``/api/bootstrap` API 응답으로 초기 데이터를 갱신하도록 변경
- SQLite 저장소 연결 완료
- 로컬 제출 이미지 폴더 연결 완료
- 기본 헬스체크 추가
- `.env` 또는 프로세스 환경변수 기반 외부 API/로컬 LLM 설정 로딩
- Naver 이미지 검색 API 연결: 수동 검색에서 텍스트 쿼리만 전송
- Google Cloud Vision Web Detection 연결: 신규 로컬 제출 분석 시 파생 이미지 전송
- 내부 Ollama 연결: 재분석 시 기존 근거 기반 LLM 요약 생성
- 증거 상태 기록: 판단에 사용, 무관, 오탐, 보류를 증거별로 저장
- 보류/반려 판정 기반 주의 후보 자동 생성
- 주의 후보 이미지 유사도 매칭, 확정 DB 편입, 오탐 제외 흐름 추가
## 실행 방법
`.env.example`을 참고해 루트에 `.env`를 만들고 필요한 키만 채운다. 키가 없는 외부 API provider는 자동으로 disabled 상태가 된다. Ollama LLM은 로컬 기본값으로 켜진다.
```text
NAVER_CLIENT_ID=
NAVER_CLIENT_SECRET=
GOOGLE_CLOUD_VISION_API_KEY=
COPYRIGHTER_GOOGLE_FACE_CROP_SEARCH=false
OLLAMA_BASE_URL=http://localhost:11434
OLLAMA_MODEL=qwen2.5:0.5b-instruct
```
```powershell
cd C:\Users\USER\Desktop\complete\copyrighter
python run_copyrighter_server.py
```
브라우저:
```text
http://127.0.0.1:9500/
```
헬스체크:
```text
http://127.0.0.1:9500/health
http://127.0.0.1:9500/api/providers/health
```
기본 저장 위치:
```text
data/copyrighter.sqlite3
data/submissions/submissions.json
data/submissions/images/
```
## env 키와 연결 동작
| env | 용도 | 연결되는 흐름 |
| --- | --- | --- |
| `NAVER_CLIENT_ID` | Naver Open API 클라이언트 ID | `POST /api/search/manual`에서 provider가 `naver`일 때 사용 |
| `NAVER_CLIENT_SECRET` | Naver Open API 클라이언트 Secret | Naver 요청 헤더에 사용 |
| `NAVER_SEARCH_DISPLAY` | 검색 결과 개수, 기본 `10` | Naver 이미지 검색 쿼리 |
| `NAVER_SEARCH_PAGES` | 이미지 검색 페이지 수, 기본 `1`, 최대 `10` | `display` 단위로 다음 결과 페이지까지 가져온다. 페이지 수만큼 API 호출량이 늘어난다. |
| `NAVER_SEARCH_SORT` | 정렬, 기본 `sim` | Naver 이미지 검색 쿼리 |
| `NAVER_BLOG_SEARCH_DISPLAY` | 블로그 검색 결과 개수, 기본 `3` | 이미지 검색이 직접 매칭을 만들지 못할 때 원문 페이지 대표 이미지를 찾는 보조 검색 |
| `NAVER_BLOG_SEARCH_PAGES` | 블로그 검색 페이지 수, 기본 `1`, 최대 `10` | 블로그 보조 검색의 다음 결과 페이지까지 가져온다. 페이지 수만큼 API 호출량이 늘어난다. |
| `NAVER_BLOG_SEARCH_SORT` | 블로그 정렬, 기본 `sim` | Naver 블로그 검색 쿼리 |
| `NAVER_WEB_SEARCH_DISPLAY` | 웹문서 검색 결과 개수, 기본 `3` | 이미지/블로그 검색이 매칭 이미지를 만들지 못할 때 일반 웹문서 페이지 대표 이미지를 찾는 보조 검색 |
| `NAVER_WEB_SEARCH_PAGES` | 웹문서 검색 페이지 수, 기본 `1`, 최대 `10` | 웹문서 보조 검색의 다음 결과 페이지까지 가져온다. 페이지 수만큼 API 호출량이 늘어난다. |
| `GOOGLE_CLOUD_VISION_API_KEY` | Cloud Vision REST API 키 | 신규 제출 seed 분석에서 Web Detection 사용 |
| `GOOGLE_CLOUD_VISION_PARENT` | 선택 project/location parent | Cloud Vision 요청 body에 선택적으로 포함 |
| `COPYRIGHTER_GOOGLE_FACE_CROP_SEARCH` | 얼굴 영역 Google Web Detection 사용 여부, 기본 `false` | 재분석 시 감지된 얼굴 영역 crop만 별도 파생 이미지로 보내 웹 근거를 수집한다. 동일인 판정이나 얼굴 인식 점수로 쓰지 않는다. |
| `GOOGLE_CUSTOM_SEARCH_IMAGE_RESULTS` | Google 이미지 검색 1페이지 결과 개수, 기본 `3` | Google Custom Search 이미지 검색 쿼리 |
| `GOOGLE_CUSTOM_SEARCH_IMAGE_PAGES` | Google 이미지 검색 페이지 수, 기본 `1`, 최대 `10` | `num` 단위로 다음 결과 페이지까지 가져온다. 페이지 수만큼 API 호출량이 늘어난다. |
| `GOOGLE_CUSTOM_SEARCH_WEB_RESULTS` | Google 웹 검색 1페이지 결과 개수, 기본 `3` | 이미지 검색이 직접 매칭을 만들지 못할 때 웹 검색 결과 페이지의 대표 이미지를 찾는다. |
| `GOOGLE_CUSTOM_SEARCH_WEB_PAGES` | Google 웹 검색 페이지 수, 기본 `1`, 최대 `10` | 웹 검색의 다음 결과 페이지까지 가져온다. 페이지 수만큼 API 호출량이 늘어난다. |
| `COPYRIGHTER_AUTO_NAVER_QUERY_LIMIT` | Google 근거에서 자동 생성해 실행할 Naver 쿼리 수, 기본 `3`, 최대 `10` | Google 페이지 제목과 엔티티를 우선순위로 정렬해 여러 텍스트 이미지 검색을 자동 실행한다. |
| `COPYRIGHTER_AUTO_NAVER_BLOG_QUERY_LIMIT` | 이미지 검색 매칭이 없을 때 추가 실행할 Naver 블로그 쿼리 수, 기본 `1`, 최대 `10` | 블로그 검색 결과 페이지의 대표 이미지를 추출해 제출 이미지와 지문 비교한다. |
| `COPYRIGHTER_AUTO_NAVER_WEB_QUERY_LIMIT` | 이미지/블로그 검색 매칭이 없을 때 추가 실행할 Naver 웹문서 쿼리 수, 기본 `1`, 최대 `10` | 웹문서 검색 결과 페이지의 대표 이미지를 추출해 제출 이미지와 지문 비교한다. |
| `COPYRIGHTER_SEARCH_RESULT_COMPARE_LIMIT` | 검색 결과 이미지 URL을 내려받아 제출 이미지와 지문 비교할 건수, 기본 `3`, 최대 `20` | Naver/Google 결과 이미지를 로컬 저장소에 저장한 뒤 pHash로 제출 이미지와 직접 비교한다. |
| `COPYRIGHTER_SEARCH_RESULT_PAGE_IMAGE_LIMIT` | 검색 결과 원문 페이지에서 추출할 대표 이미지 수, 기본 `3`, 최대 `10` | 결과 자체에 이미지 URL이 없으면 `og:image`, `twitter:image`, `img` 후보를 제한된 수만 내려받아 제출 이미지와 비교한다. |
| `COPYRIGHTER_SEARCH_RESULT_SIMILARITY_THRESHOLD` | 검색 결과 이미지 유사도 필터 임계치, 기본 `0.9`, 범위 `0.0`~`1.0` | Naver/Google 결과 후보 이미지의 pHash 유사도 기반 매칭 임계치를 조절한다. |
| `OLLAMA_BASE_URL` | Ollama 로컬 서버 URL, 기본 `http://localhost:11434` | `POST /api/submissions/{id}/rerun-enrichment`에서 LLM 요약 사용 |
| `OLLAMA_MODEL` | Ollama 모델, 기본 `qwen2.5:0.5b-instruct` | Ollama `/api/generate` payload의 `model` |
| `COPYRIGHTER_NAVER_DAILY_LIMIT` | Naver 일일 호출 제한 | 로컬 policy gate |
| `COPYRIGHTER_GOOGLE_DAILY_LIMIT` | Google 일일 호출 제한 | 로컬 policy gate |
| `COPYRIGHTER_LLM_DAILY_LIMIT` | LLM 일일 호출 제한 | provider 상태 표시용 |
환경변수는 `.env`보다 우선한다. 예를 들어 PowerShell에서 이미 `$env:OLLAMA_MODEL`이 있으면 `.env` 값으로 덮어쓰지 않는다.
## 공식 문서 기준
- Naver 이미지 검색 API: https://developers.naver.com/docs/serviceapi/search/image/image.md
- Google Cloud Vision Web Detection: https://cloud.google.com/vision/docs/detecting-web
- Ollama Generate API: https://docs.ollama.com/api/generate
- qwen2.5 0.5B Instruct 모델: https://ollama.com/library/qwen2.5:0.5b-instruct
## 현재 API
```text
GET /health
GET /api/providers/health
GET /api/bootstrap
GET /api/review-queue
GET /api/submissions/{submission_id}/review
POST /api/submissions/reload
POST /api/submissions/{submission_id}/rerun-enrichment
POST /api/submissions/{submission_id}/decision
POST /api/evidence/{evidence_id}/status
POST /api/knowledge/{entry_id}/promote-watchlist
POST /api/knowledge/{entry_id}/exclude-watchlist
POST /api/search/manual
GET /api/providers
PATCH /api/providers/{provider_id}
POST /api/providers/emergency-disable
GET /api/audit-events
GET /media/{image_path}
```
## 로컬 이미지 저장 방식
제출 이미지는 두 방식으로 넣을 수 있다.
- 빠른 방식: 이미지 파일을 `data/submissions/images/` 아래에 복사한 뒤 화면에서 `새 제출 불러오기`를 누른다. 이 경우 제출 ID와 제목은 파일명 기준으로 자동 생성된다.
- 명시 방식: `data/submissions/submissions.json`에 ID, 제목, 크기, 제출 시간을 직접 등록한 뒤 화면에서 `새 제출 불러오기`를 누른다.
예시:
```json
[
{
"id": "SUB-LOCAL-001",
"title": "로컬 얼굴 이미지 샘플",
"file": "images/local-face.svg",
"width": 1200,
"height": 900,
"submitted_at": "2026-05-26 10:00"
}
]
```
이미지 파일은 `data/submissions/images/` 아래에 둔다. 서버는 이 폴더 밖으로 나가는 경로를 거부한다.
서버를 재시작하지 않아도 된다. 운영 콘솔의 심사 큐 상단에서 `새 제출 불러오기`를 누르면 `submissions.json` 변경분과 폴더에 새로 복사한 이미지가 SQLite DB로 import된다.
## 운영자 판정 흐름
1. 제출 이미지를 선택하고 상단의 `선택 재분석` 또는 행 안의 증거를 확인한다.
2. 증거 행에서 `판단에 사용`, `무관`, `오탐`, `보류`를 표시한다. 이 표시는 케이스 기록과 점수 반영 여부를 정리할 뿐, 기준 DB 후보를 만들지는 않는다.
3. 케이스 판정을 먼저 내린다. `승인`은 자동 후보를 만들지 않는다. `보류``반려`는 제출 이미지 지문과 선택된 근거를 묶어 `주의 후보`를 만든다.
4. 이후 같은 이미지가 들어오면 `주의 후보 근거` 그룹에 별도로 표시되고 높은 위험 신호로 반영된다. 그래도 최종 판정은 자동 변경되지 않는다.
5. 지식 DB 화면에서 주의 후보를 검토한 뒤 `확정 DB 편입` 또는 `오탐 제외`를 누른다. 제외된 후보는 다음 내부 유사도 분석에서 사용하지 않는다.
## 외부 API 연결 상태
연결 완료:
- Naver: `https://openapi.naver.com/v1/search/image``GET` 요청을 보낸다. `query`, `display`, `start`, `sort`를 쿼리스트링으로 보내고 `X-Naver-Client-Id`, `X-Naver-Client-Secret` 헤더를 사용한다.
- Google: `https://vision.googleapis.com/v1/images:annotate``WEB_DETECTION` 요청을 보낸다. 서버는 내부 파생 이미지 bytes를 base64로 인코딩해 전송한다.
- Provider 상태: 외부 API는 키가 있으면 enabled, 없으면 disabled와 missing env 사유를 `/api/providers`에 표시한다.
- Provider Controls: 화면의 provider 활성/비활성 상태는 DB에 저장된다.
아직 운영 검증이 필요한 것:
- 실제 운영 키로 Naver/Google 샘플 호출 품질 확인
- 공급자별 재시도/backoff 정책
- 일일 한도 초과 시 관리자 알림
- API 키를 Windows 서비스/작업 스케줄러/배포 환경의 시크릿 저장소로 옮기는 운영 방식
운영 경계:
- Naver에는 텍스트 쿼리만 보낸다.
- Naver로 원본 이미지나 파생 이미지를 보내지 않는다.
- Google에는 승인된 파생 이미지만 보낸다.
- Google Cloud Vision 사용 전 계약, DPA, 데이터 보존, 메타데이터 로깅, 리전, 자격 증명 정책을 확인한다.
## LLM 연결 상태
연결 완료:
- Ollama 로컬 API에 `POST /api/generate` 요청을 보낸다.
- 기본 URL은 `http://localhost:11434`, 기본 모델은 `qwen2.5:0.5b-instruct`다.
- Ollama 요청은 API 키를 쓰지 않는다.
- 응답 스트리밍은 끄고 `stream: false`로 한 번에 요약을 받는다.
- `rerun-enrichment`는 이미 저장된 내부/Naver/Google 근거만 LLM 입력으로 넘긴다.
- LLM 요약은 `source_evidence_ids` 또는 출처 URL을 가진 보조 근거로만 저장한다.
- LLM 실패는 기존 근거를 낮추지 않고 failure evidence로 남긴다.
설치 명령:
```powershell
ollama pull qwen2.5:0.5b-instruct
```
아직 운영 검증이 필요한 것:
- 실제 운영 PC에서 `ollama pull qwen2.5:0.5b-instruct` 후 샘플 재분석 호출 품질 확인
- 프롬프트/응답 로그 저장 여부와 마스킹 정책 확정
- LLM 요약을 신청자에게 노출하지 않는 정책 재검증
LLM이 해도 되는 일:
- 검색 쿼리 후보 생성
- 증거 요약
- 중복/상충 증거 정리
- 운영자가 읽기 쉬운 근거 메모 생성
LLM이 하면 안 되는 일:
- 최종 승인/보류/반려 결정
- 단독 점수 산정
- 출처 없는 유명인/작품/IP 단정
- 신청자에게 보여줄 자동 설명 생성
## 아직 안 된 것: 운영용 인증과 배포
현재 9500 서버는 로컬 실행용이다. 실운영 전에 아래가 필요하다.
- 로그인/세션 또는 사내 SSO
- 일반 운영자와 관리자 권한 분리
- Provider Controls 관리자 전용 접근
- 운영자 결정/보정/지식 DB 변경 감사 로그 강화
- 9500 서버 프로세스 관리 방식 확정
- 백업, 복구, 모니터링, 알림
## Google Custom Search 설정
| env | 용도 | 연결되는 흐름 |
| --- | --- | --- |
| `GOOGLE_CUSTOM_SEARCH_API_KEY` | Google Programmable Search JSON API 키 | Google Vision 텍스트 단서를 Google 이미지/웹 검색으로 확장 |
| `GOOGLE_CUSTOM_SEARCH_CX` | Programmable Search Engine ID | Custom Search JSON API `cx` 값 |
| `GOOGLE_CUSTOM_SEARCH_IMAGE_RESULTS` | 이미지 검색 결과 개수, 기본 `3` | 텍스트 쿼리 기반 이미지 결과를 내려받아 제출 이미지와 pHash 비교 |
| `GOOGLE_CUSTOM_SEARCH_WEB_RESULTS` | 웹 검색 결과 개수, 기본 `3` | 이미지 검색이 비었을 때 웹 결과 페이지 대표 이미지를 비교 |
| `COPYRIGHTER_AUTO_GOOGLE_CUSTOM_QUERY_LIMIT` | 자동 Google Custom Search 쿼리 수, 기본 `2`, 최대 `10` | 제출 이미지를 보내지 않고 텍스트 쿼리만 전송 |
| `COPYRIGHTER_GOOGLE_CUSTOM_SEARCH_DAILY_LIMIT` | Google Custom Search 일일 호출 제한 | 로컬 policy gate |
자동 검색 쿼리 원천:
- Google 페이지 제목과 엔티티가 가장 높은 우선순위다.
- Google best guess label만 있는 경우에도 `person`, `gentleman`, `portrait` 같은 일반어는 버리고, `IU official profile`처럼 구체적인 문구만 낮은 우선순위 쿼리로 사용한다.
- 얼굴 영역 Google Web Detection은 동일 인물 판정 근거로 쓰지 않는다. 다만 페이지 제목이나 엔티티가 구체적이면 낮은 우선순위의 텍스트 검색 쿼리로만 재사용한다.
- 페이지 제목이 영어권 문구면 보조 쿼리에 `image`를 붙이고, 한국어 문구면 `이미지`를 붙인다. 검색엔진에 깨진 한글 템플릿을 보내지 않는다.
- Google Custom Search와 Naver에는 제출 이미지를 보내지 않고 텍스트 쿼리만 보낸 뒤, 검색 결과 이미지나 원문 페이지 대표 이미지를 내려받아 로컬에서 pHash 비교한다.
설정 진단:
- `/api/providers/health`와 공급자 화면은 `requiredEnv`, `configuredEnv`를 비밀값 없이 표시한다.
- `google_search`가 disabled이면 우선 `GOOGLE_CUSTOM_SEARCH_API_KEY`, `GOOGLE_CUSTOM_SEARCH_CX`가 둘 다 `.env`에 있는지 확인한다.