# 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`에 있는지 확인한다.