maskingtape
Health Warn
- License — License: Apache-2.0
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Low visibility — Only 8 GitHub stars
Code Warn
- network request — Outbound network request in apps/api/maskingtape_api/body_limit.py
Permissions Pass
- Permissions — No dangerous permissions requested
No AI report is available for this listing yet.
한국어 개인정보 비식별화 오픈소스 엔진 — 규칙+로컬 LLM 하이브리드 PII 탐지·마스킹 라이브러리, CLI, MCP 서버 (2026 오픈소스 개발자대회, 팀 마스킹테이프)
마스킹테이프 maskingtape
한국어 개인정보 비식별화 오픈소스 엔진 — 규칙 기반 탐지 + 로컬 LLM 문맥 판단 하이브리드
한국어 문서·데이터셋에서 개인정보(주민등록번호·전화번호·주소·이름 등)를 탐지해 마스킹·가명처리하는 Python 라이브러리 + CLI + MCP 서버.
AI 에이전트가 한국어 데이터를 다루기 전에 거치는 프라이버시 계층을 목표로 한다.
라이브 데모 · PyPI (pip install maskingtape) · 개발 로드맵 · 기여 가이드
2026 오픈소스 개발자대회(과학기술정보통신부 주최·NIPA 주관) 출품작 — 팀 마스킹테이프 · Apache-2.0
English Summary
maskingtape is an Apache-2.0 Korean PII detection and anonymization toolkit. It provides
a Python library, CLI, MCP server, REST API, web playground, desktop app, and reproducible
synthetic benchmarks from one shared core engine.
- Built for Korean identifiers and writing conventions: resident registration numbers, Korean
phone numbers, road-name addresses, Korean names, cards, accounts, business registration
numbers, passports, birth dates, and driver licenses. - Runs locally by default. The core library, CLI, MCP server, and desktop app do not call
commercial AI APIs. Optional name disambiguation uses local Ollama. - The web demo backend has one optional exception: its hybrid mode sends the rule-masked text
(not the original) to the OpenAI API to find names the rules missed. It is off by default, and
any value the rules missed stays in that text. See where the text goes. - Includes a public demo for quick exploration. Do not enter real personal data in the demo;
use the local CLI, MCP server, or desktop app for sensitive documents. - Contributions are issue-driven. Start with CONTRIBUTING.md, use synthetic
data only, and record the tests or benchmark commands you ran.
왜 maskingtape인가
- 한국어 전용: 주민등록번호(체크섬 검증 포함)·한국 전화번호·도로명 주소·한국어 이름 등 국내 포맷 특화 — 영어권 도구(Presidio 등)가 못 채우는 갭
- 하이브리드 탐지: 정규식·사전 규칙(빠름, 결정적) + 로컬 LLM 문맥 판단(인명 vs 상호명 구분 같은 애매한 케이스, 선택 사항)
- 완전 로컬: 라이브러리·CLI·MCP 서버·데스크톱 앱은 외부 API를 부르지 않는다. LLM도 Ollama 기반 오픈웨이트 모델만 사용 — 개인정보가 밖으로 나가지 않는다. 시연용 웹 데모만 예외로, 하이브리드 모드를 고르면 규칙으로 가린 글을 OpenAI로 보낸다(원문은 어디로 가나)
- 규칙 전용 모드: LLM 없이도 동작 — 저사양 환경에서도 쓸 수 있다
- MCP 서버: AI 에이전트 워크플로에 비식별화 계층을 끼워 넣을 수 있다
빠른 시작
필요한 것: Python 3.10 이상. 그게 전부다 — 코어 엔진과 CLI는 표준 라이브러리만 쓰고, 외부 서비스도 부르지 않는다.
pip install maskingtape
maskingtape "주민번호 800101-1234560 문의주세요"
# → 주민번호 ************** 문의주세요
maskingtape --strategy label "연락처 010-1234-5678" # → 연락처 [전화번호]
maskingtape --scan "주민번호 800101-1234560 문의주세요" # 탐지 리포트(JSON)만
소스로 받아 개발하려면(테스트·린트 포함):
git clone https://github.com/ChoHyeonChan/maskingtape.git
cd maskingtape && pip install -e "packages/core[dev]"
pytest packages/core
설치는 항상 개별 패키지 경로로 한다 — 저장소 루트에는 배포용
[build-system]이 없어(uv 워크스페이스 전용)pip install .은 루트에서 실패한다. 위 예시처럼packages/core,packages/mcp-server,apps/api를 각각 지정하면 된다.
로컬 LLM 기능은 선택 사항이며 설치 방법은 packages/core에 있다. 웹 데모는 Node.js 22.13 이상의 22.x 또는 24 이상(CI는 24), 데스크톱 앱은 Flutter가 추가로 필요하며 각 폴더 README를 참고한다.
현재 탐지(11종): 주민등록번호(체크섬 검증), 전화번호(휴대폰·유선·070·050X, +82 표기), 이메일, 주소(행정구역·도로명), 신용카드(Luhn 검증), 계좌번호, 사업자등록번호, 여권번호, 생년월일, 운전면허번호, 이름(규칙 + 로컬 LLM 문맥 판단)
라이브러리로 쓰기:
from maskingtape import Pipeline
result = Pipeline().anonymize("주민번호 800101-1234560 문의주세요")
print(result.text) # 주민번호 ************** 문의주세요
print(result.detections) # [Detection(kind='rrn', start=5, end=19, ...)]
※ 예시의 주민등록번호는 체크섬만 맞춘 합성 번호다.
마스킹 방식 세 가지
| 방식 | 결과 | 쓰는 자리 |
|---|---|---|
mask(기본) |
주민번호 ************** |
값을 완전히 지울 때 |
label |
주민번호 [주민등록번호] |
무엇이 있었는지는 남겨야 할 때 |
pseudonym |
주민번호 310615-4145804 |
문장 구조를 살려 LLM 전처리·데이터셋 공유에 넘길 때 |
가명처리는 "그럴듯한 가짜 값"이라 실존 정보와 겹치지 않는지가 곧 안전성이다.
세 가지를 설계에 박아 뒀고, 아래는 전부 실제로 돌려 확인한 값이다.
- 전역 결정적 매핑을 두지 않는다. 같은 입력을 두 번 돌리면 다른 가명이 나온다.
"같은 원본 → 항상 같은 가명"을 두면 그 매핑 테이블 자체가 원본 복원 수단이 된다. - 주민등록번호·카드번호는 체크섬을 일부러 통과하지 않게 만든다. 진짜 탐지기의 검증
함수를 재사용해 "검증하면 가짜"임을 보장한다 — 300회 생성한 가짜 주민등록번호 중
체크섬 통과 0건, 가짜 카드번호 중 Luhn 통과 0건. 유효한 번호를 만들어 내면
실존 인물의 것과 겹치거나 그 자체로 악용될 수 있다. 다만 2020년 10월 이후 발급된
주민등록번호는 뒷자리가 난수라 체크섬이 없어서, 체크섬이 틀린 가짜라도 그 번호들과는
우연히 겹칠 수 있다. - 가명은 원본을 드러내지 않게 뽑는다. 원본과 같거나 원본을 품은 가명(
임하→임하은)은
다시 뽑고, 같은 호출의 다른 가명과도 겹치지 않게 한다. 두 사람이 같은 가명을 받으면 문맥이 섞인다.
이름이 수백 개라 가명 어휘(400개)가 바닥나면 나머지 이름은[이름]라벨로 가린다. - 한 번의 호출 안에서는 같은 원본이 같은 가명으로 바뀐다.
김소연 대리와 김소연 과장
→신채원 대리와 신채원 과장. "그 사람"이라는 문맥은 살아 남는다.
그래도 생성된 값은 형식이 그럴듯해 실제 정보와 우연히 닮을 수 있다. 반드시 가짜
데이터로만 취급한다.
정확도 (공개 벤치마크)
저작권·개인정보 걱정 없는 자체 합성 데이터셋으로 정확도를 측정한다 — 공개 벤치마크는 이 프로젝트의 핵심 차별화 포인트다. 아무나 다음 한 줄로 재현할 수 있다:
python -m bench.evaluators.evaluate bench/datasets/synth_v1.jsonl
합성 데이터셋(500건)도 시드로 고정돼 있어 바이트 단위로 똑같이 재생성된다 — python -m bench.generate_dataset --count 500 --seed 42 --out bench/datasets/synth_v1.jsonl
측정 기준: #537(업무 문서 제목형 라벨 어휘 추가)+#484(약한 단서 오탐 4종 제거) 병합 시점 main · 규칙 전용 모드(LLM 미사용), 2026-10-04 재측정(#607·#650 반영, main c526000), 2026-10-08 main 97ed96f에서 다시 돌려 같은 값, 2026-10-15 #601(두 글자 이름 + 띄어 쓴 직함) 반영 재측정(이름 recall 0.932 → 0.956, 오탐 12건 그대로) — 이름 외 10종은 #370 시점(#339/#340 표기 변형 반영)과 동일하다
| 종류 | precision | recall | F1 | F2 |
|---|---|---|---|---|
| 주민등록번호 | 1.000 | 1.000 | 1.000 | 1.000 |
| 전화번호(휴대폰+유선+050X) | 1.000 | 1.000 | 1.000 | 1.000 |
| 이메일 | 1.000 | 1.000 | 1.000 | 1.000 |
| 주소 | 1.000 | 1.000 | 1.000 | 1.000 |
| 신용카드번호 | 1.000 | 1.000 | 1.000 | 1.000 |
| 사업자등록번호 | 1.000 | 1.000 | 1.000 | 1.000 |
| 여권번호 | 1.000 | 1.000 | 1.000 | 1.000 |
| 계좌번호 | 1.000 | 1.000 | 1.000 | 1.000 |
| 생년월일 | 1.000 | 1.000 | 1.000 | 1.000 |
| 운전면허번호 | 1.000 | 1.000 | 1.000 | 1.000 |
| 이름 (규칙 전용) | 0.968 | 0.956 | 0.962 | 0.958 |
| 전체 | 0.988 | 0.983 | 0.985 | 0.984 |
#607부터 카드·사업자등록번호 라벨이 번호 바로 앞에 있으면 체크섬이 틀린 번호도 확신도 0.6으로 가린다
(오타 난 실제 번호일 수 있어서다). 합성 데이터에서 그 자리에 놓인 "사업자등록번호 …" 오답지 3건(v1 2건)은
#650에서 정답으로 채점 기준을 바꿨다 — 글은 그대로이고 라벨만 늘었다(사업자등록번호 tp 20 → 22).
F2(재현율에 F1보다 더 큰 가중치를 두는 Fβ, β=2)를 F1과 나란히 보는 이유는 PII 탐지 도메인에서
미탐(FN)이 오탐(FP)보다 위험하기 때문이다(자세한 근거는 bench/README.md 참고) —evaluate.py가 이미 계산해 내놓는 값을 그대로 옮겼다.
번호·카드·사업자등록번호·여권번호·계좌번호·생년월일은 형태(와 있는 경우 체크섬)로 완전히
잡힌다(유선전화·plus 이메일·서브도메인·여러 문장으로 구성된 복합 문서까지 섞어도 흔들리지
않음을 확인했다) — 사업자등록번호는
#123, 여권번호는
#139, 계좌번호는
#180, 생년월일은
#266/#271에서
각각 새 kind를 추가해 측정 사각지대를 없앴다 — 생년월일은 10번째 kind이자 계좌번호와 같은
문맥 앵커 하드 게이트 설계라, confidence가 항상 정확히 0.9로 고정된다(임계값 0.91
이상이면 100% 소실, 아래 참고). 운전면허번호는 #267에서
core가 추가한 11번째 kind로, 체크섬이 비공개라 형식+지역코드(11~26·28)만으로 판단해
confidence가 항상 정확히 0.85로 고정되고 — 계좌·생년월일과 달리 문맥 앵커 게이트조차
없다. 즉 우연히 유효 지역코드로 시작하는 임의의 12자리 숫자는 문맥과 무관하게 무조건
운전면허번호로 오탐된다는 뜻이라, #315에서
벤치 데이터를 채우다가 계좌번호 생성기 자신의 출력(구분자 없는 12자리)이 정확히 이 경계에
두 번 걸리는 걸 실측으로 찾았다 — 한 번은 이 운전면허 지역코드 자체와, 한 번은 우연히도
core PhoneDetector의 050 평생번호 정규식과. 둘 다 생성기에 회귀 가드를 추가해 항상
피하도록 고쳤다(카드 Luhn 우연 통과 가드와 같은 패턴). 주소는 #195/#196에서
시/도명 뒤 조사 미탐 버그를, #248에서
계사 어미("입니다"/"예요") 미탐도 찾아 core #252가
전부 고쳐 recall이 1.000으로 완전히 복구됐다(직접 재확인함). 주민등록번호는 #159에서
체크섬 없는(2020-10 이후 발급분) 케이스, #209에서
점(.) 구분자 표기도 섞었는데 precision/recall엔 영향이 없다 — core가 형식만으로 탐지 자체는
하기 때문(체크섬 없는 케이스는 confidence가 0.85로 낮아져 임계값 필터를 쓰면 새기 쉽다,
bench/ 참고). 전화번호는 #211에서
050X 평생번호·안심번호도 새로 잡게 됐다. 계좌번호는 core AccountDetector가 문맥어(계좌/입금/은행 등)가
없으면 아예 탐지를 안 하는 하드 게이트인 데다, 체크섬이 없어 confidence가 항상 정확히 0.6으로
고정된다 — confidence 임계값을 0.7 이상으로만 올려도 계좌번호가 전량 걸러진다는 걸 확인했다
(bench/ 참고). 여권번호는 core에 체크섬 검증이 없어(형식+문맥어만으로 판단) distractor가
우연히 형식과 겹치지 않는지 별도 회귀 테스트로 고정해뒀다. 이름 규칙판은 #213(직함이
이름 뒤)·#239(직함이 이름 앞) 둘 다
지원하게 됐고, 그 오탐 방지 가드가 조사에 뚫리던 버그(#247)도
core #252가 고쳤다. 하드코딩 5개 부서어만
막는 방식이라 그 밖의 흔한 업무어("차량"/"허가" 등)가 #255에서
여전히 뚫리던 잔여 위험(negative 문서 오탐 23건)은 #394/#399 규칙판 정비가 정지어를 일반명사
목록으로 넓히고 단어 경계로 걸러 0건으로 없앴다(precision 0.859 → 0.954). 같은 정비에서 실무
직함(총무·매니저·간호사·변호사 등, #394)과
서식 역할어(환자명·명의자·예금주·서명자 등)를 어휘에 넣고 역할어 뒤 조사("담당자는 X")를 처리해
recall도 0.668 → 0.869로 올랐다 — 남은 과제는 문맥 단서가 없는 이름과 2음절 이름+조사다(2음절 이름 + 띄어 쓴 직함은 #601에서 고쳤다):
- 이름 — 한국어 이름은 형태만으로 구분되지 않아 규칙만으로는 문맥 없는 이름을 놓친다. 이것이 로컬 LLM 하이브리드(
--llm)가 필요한 이유이고, 그 효과는 #46에서 같은 데이터셋으로 비교 측정한다 — 2026-10-01 기준(#537+#484 반영 후, 당시 기본 모델인 로컬 Ollama qwen2.5:7b) 규칙 전용 F1 0.949(precision 0.967 · recall 0.932, 미탐 26건)가 하이브리드 F1 0.934(precision 0.911 · recall 0.958, 미탐 16건)를 오히려 앞선다 — recall은 하이브리드가 더 높지만 오탐이 3배 늘어(12→36) F1에서는 손해다. recall에 더 큰 가중치를 두는 F2는 반대로 하이브리드가 앞선다(0.948 vs 0.939, 상세는 bench/README.md). 벤치 v2(이름 366개)는 규칙 전용 F1 0.956이다(이전 0.926에서 상승, #537+#484 반영). 기본 모델 1.5B로 v1·v2·표기 변형·문서 성격별 세트를 함께 잰 표는 bench/README.md 「종합 비교표」(#548)에 있다. 1.5B는 bench 생성기 데이터로 학습해서 합성 세트(v1·v2)의 하이브리드 수치는 부풀려져 있다. 하이브리드는 규칙판을 안전망으로 함께 돌려(#476부터 확신도 제한 없이), LLM이 놓친 이름도 규칙이 단서로 잡았다면 가려진다. ※ 하이브리드 수치는 온도 0으로 1회 측정한 값이고, Ollama·모델 버전이 바뀌면 달라질 수 있다(규칙 전용 지표는 결정적이라 그대로 재현된다). 다만 표(CSV·TSV·마크다운)에서 머리행에 열 이름("이름"·"성명"·"고객명" 등)이 있고 값은 아래 행에 나열되는 고객 명단 같은 구조는 규칙 전용도 그 열의 값을 전부 이름 후보로 보므로(#526) 놓치지 않는다 — 같은 줄에 라벨과 값이 있는 양식 칸과 같은 기준을 쓴다. - LLM 모델을 바꿔도 거의 같다 — 같은 조건에서 Gemma 4 E4B(Apache-2.0)와 비교하면 v1+v2 이름 749개 기준 F1 0.947(qwen2.5:7b) 대 0.952로, 차이는 이름 3개였다. 개발 노트북 CPU에서 Gemma가 문서당 약 2.7배 느려 qwen2.5:7b를 유지했다(2026-09-30).
- 기본 모델은 우리가 학습한 1.5B다(2026-10-04부터). Qwen2.5-1.5B-Instruct를 합성 데이터로 파인튜닝한
maskingtape-name-1.5b(986MB, #458, 허깅페이스 공개)를 기본으로 쓴다. 이 모델은 bench 생성기로 만든 데이터로 학습해서 위 bench 수치로는 비교하지 않는다. 대신 학습에 쓰지 않은 외부 데이터인 KDPII(한국어 대화 개인정보 데이터셋, Zenodo 10.5281/zenodo.10968609, CC-BY-4.0) test에서 이름이 든 문장 169개 전부와 이름 없는 문장 331개(시드 고정)를 뽑은 500문장(이름 177개)으로 하이브리드를 비교했다. 재현율은 둘 다 0.825(이름 146개)로 같고, 정밀도는 qwen2.5:7b가 높다(0.527 대 0.471, 오탐 131 대 164). F1은 0.643 대 0.600이다. 대신 1.5B는 크기가 1/5이고 약 3배 빠르다(개발 노트북 CPU에서 같은 50문장을 두 모델로 번갈아 두 번씩 잰 값, 144148초 대 4852초). 기본 모델은 1.5B이고, 7B는--llm-model qwen2.5:7b로 고를 수 있다. 같은 500문장에서 규칙 전용은 재현율 0.254다. 대화체 이름은 문맥 단서가 없어 규칙만으로는 대부분 놓친다. 데이터는 평가에만 쓰고 저장소에 넣지 않았다(출처는 SBOM 부록 D, 2026-10-04, main8fc7889, Ollama 0.35.1, 온도 0 1회 측정).
마스킹 결과에 개인정보가 실제로 남는지도 따로 측정한다 — python -m bench.evaluators.evaluate_masking bench/datasets/synth_v1.jsonl. 상세는 bench/ 참고.
지원 범위와 한계
비식별화 도구에서 못 잡은 하나는 그 자체로 유출이다. 그래서 무엇을 보장하고 무엇을
보장하지 않는지 먼저 밝힌다. 아래 미탐 표기는 전부 직접 실행해 확인한 값이다.
| 지원 | 지원하지 않음 | |
|---|---|---|
| 입력 형식 | 텍스트(str) | docx·이미지. PDF는 웹 데모·데스크톱 앱에서 글자만 뽑아 처리한다(가린 PDF는 만들지 않음) |
| 탐지 종류 | 11종(주민등록번호·전화·이메일·주소·카드·계좌·사업자번호·여권·생년월일·운전면허·이름) | 그 밖의 식별자(사번·학번·차량번호·IP 등) |
| 이름 | 로컬 LLM(--llm, 기본 maskingtape-name-1.5b)을 켜면 단서 없는 이름을 더 잡는다. 학습에 쓰지 않은 외부 대화 데이터 KDPII에서 재현율 0.254 → 0.825, 문서 성격별 세트에서 0.892 → 0.975(템플릿만 학습 밖) |
규칙 전용은 재현율 0.956 — 스무 개 중 하나쯤을 놓친다(문맥 단서가 없는 이름, 2음절 이름+조사, 붙여 쓴 직함). 합성 벤치 기준이라 실제 문서에서는 더 놓칠 수 있다(아래 「정확도 수치를 읽는 법」) |
| 언어 | 한국어 표기를 전제로 설계 | 영문·중문·일문 이름과 주소 |
알려진 미탐 표기
지금 놓치는 표기를 그대로 적어 둔다. 각각 탐지기 한 파일만 고치면 되는 일이라,
기여를 시작하기 좋은 지점이기도 하다(기여 가이드).
| 놓치는 입력 | 잡히는 입력 | 종류 |
|---|---|---|
1234567891 |
123-45-67891 |
사업자등록번호 — 구분자 없음 |
우리 홍길동 1002-123-456789, 1002-123-456789 우리 홍길동 |
우리 1002-123-456789, 1002-123-456789 (우리) |
계좌번호 — "우리"·"하나"·"국민"처럼 일상어와 겹치는 은행 이름은 번호 바로 앞이나 번호 뒤 괄호 안에 있을 때만 문맥으로 본다(신한 홍길동 110-…처럼 헷갈리지 않는 이름은 잡는다) |
계좌 110 123-456789, 계좌 110,123,456789 |
계좌 110 123 456789, 계좌 110-123-456789 |
계좌번호 — 한 번호 안에서 구분자를 섞어 쓴 표기(구분자가 바뀌면 서로 다른 숫자로 본다)와 쉼표 표기(금액의 천 단위 구분과 가릴 수 없다) |
이름 류서윤, 담당자 탁서윤 과장 |
성명: 류서윤, | 이름 | 탁서윤 | |
이름(규칙 전용) — 성씨 사전 밖 성씨(류·변·탁 등)는 이름 전용 양식 라벨(성명·이름·예금주 등) 뒤에 쌍점·세로줄이 있을 때만 받는다. 공백만 있는 문장까지 받으면 "이름 표기 규칙" 같은 일반 낱말이 이름으로 잡힌다. --llm이면 LLM이 문맥으로 판단한다 |
강남구 테헤란로 123 근처 |
주소: 강남구 테헤란로 123, 서울 강남구 테헤란로 123 |
주소 — 시/도·시 없이 구로 시작하는 주소는 앞에 주소 단서(주소·배송지·거주지 등)가 있을 때만 받는다. "인구 이동 1.2%" 같은 통계 문장과 모양이 같아서다 |
구분자 없는 사업자등록번호(10자리 연속 숫자)는 다른 숫자와 구분이 어려워 의도적으로
잡지 않는다. 이 표와 아래 "뺀 항목"은 PyPI 0.4.0(2026-10-07)과 저장소 main 기준이다(둘의 core 코드가 같다). 이 표 밖에서 새로 찾은 미탐은 bug 라벨이 붙은 열린 이슈에 재현 입력과 함께 모아 둔다.
이 표에서 뺀 항목: 시/도 축약형(#396), 전화 괄호 표기(#397), 여권 소문자(#398),
이메일 한글 로컬파트(#400), 주소가 동/도로명 자리에서 끊겨 건물번호가 새던 표기(#423),
생년월일 2자리 연도(#399), 시·군 뒤에 바로 도로명이 오는 주소(#425),
세종 축약형과 시/도·시 뒤에 구를 붙여 쓴 주소(#465), 공백·점으로 나눈 계좌번호(#474).
모두 코드와 회귀 테스트로 고쳤다. 표에 없던 흔한 표기도 전수 감사에서 찾아 같은 방식으로 고쳤다:
라벨 뒤:·표 칸·괄호 설명이 붙은 이름(#491), 조각 사이 공백이 여러 칸이거나 줄바꿈(CRLF)이
섞인 주소, 주소 단서 뒤 구로 시작하는 주소, "종로1가 새솔로 12"처럼 N가 동 뒤에 도로명이 오는
주소(#492), 번지 뒤 건물명·층·호·참고항목 괄호가 남던 주소 꼬리(#605), 건물명과 동·호만 쓴 주소(#593),
생년월일 공문서 표기·붙여 쓴 날짜, 닫는 괄호만 쓴 전화·+82 유선, 앞뒤에 하이픈이
붙은 계좌, 19자리·14자리·구분자가 섞인 카드, 띄어 쓴 여권번호(#493), 8자리 앞자리 주민등록번호
(19800101-1234567)와 괄호 설명이 두 개 붙은 생년월일(#508), 국가번호를 괄호로 감싼 휴대폰((+82) 10-…)과+82뒤에 지역번호 0을 붙인 유선(#509), 이어 쓴 카드 두 번호의 뒤 묶음(#510), 머리행에만 열 이름이 있는
표의 이름 값(#526), 뒷자리를*·X등으로 가린 주민등록번호의 앞자리(#528),/···_로 나눈
주민등록번호(#529), "이력서 접수: 전혜호, 생일 …"처럼 업무 문서 제목형 라벨 뒤 이름(#537),
"참석자: 김민수, 이도현, 박서준"처럼 라벨 뒤에 나열한 이름의 둘째 이름부터(#602).
정확도 수치를 읽는 법
위 표의 F1 1.000은 이 저장소의 합성 생성기가 만든 표기에 대한 값이다. 생성기가 코어의
형식 검증 코드를 그대로 가져다 쓰기 때문에, 구조화된 10종의 1.000은 "탐지기가 아는 형식을
정확히 그 경계까지 잡아내는가"를 보는 자기일관성 지표에 가깝다. 실세계 표기의 다양성을
모두 담지는 못한다.
이름도 #394 정비로 이 순환에 부분적으로 들어왔다 — 새로 넣은 서식 역할어 34개 중 11개가
생성기 템플릿이 쓰는 단어와 같아, 그 11개를 빼고 재측정하면 F1이 0.910 → 0.802, recall이
0.869 → 0.702로 내려갔다(precision 0.954는 어휘를 모두 빼도 0.934로 유지돼 구조 개선의 효과가 맞다).
그 뒤 #537에서 더한 업무 문서 제목형 라벨("이력서 접수:"·"면허 갱신 신청:")도
생성기 템플릿에 나오는 문구다. 그래서 지금의 규칙 전용 recall 0.956도 실세계 추정치가 아니라 상한이다.
자세한 내용은 bench/README.md의 한계 고지를 참고한다.
원문은 어디로 가나
개인정보를 가리는 도구가 가리기 전의 원문을 밖으로 보내면 쓸 이유가 없다. 쓰는 방법마다 원문이
어디까지 가는지 적는다. 아래는 전부 코드로 확인한 동작이다.
flowchart LR
subgraph pc["사용자 PC: 원문이 머무는 곳"]
doc["원문 문서·텍스트"]
cli["CLI · Python 라이브러리"]
desk["데스크톱 앱"]
llm["Ollama<br/>--llm일 때만"]
mcp["MCP 서버<br/>stdio"]
copy["가린 사본 파일"]
end
subgraph agent["AI 에이전트"]
ag["에이전트<br/>클라우드 LLM일 수 있음"]
end
subgraph demo["웹 데모"]
browser["브라우저<br/>파일은 여기서 글자만 뽑음"]
api["우리 API 서버(Vercel)<br/>기본은 규칙만 씀<br/>우리 코드는<br/>저장·로그 안 함"]
end
openai["OpenAI API<br/>하이브리드 모드를<br/>고를 때만"]
doc --> cli
doc --> desk
desk -- "로컬 CLI 호출<br/>없으면 이 PC의 API" --> cli
cli -. "로컬 주소만 허용" .-> llm
mcp -. "로컬 주소만 허용" .-> llm
doc -- "PC 안에서 읽음" --> mcp
ag -- "anonymize_file: 경로만" --> mcp
ag -- "anonymize_text: 이미 가진 글자" --> mcp
mcp --> copy
mcp -- "경로·건수 또는 가린 글자" --> ag
copy -- "가린 사본" --> ag
doc -- "붙여넣기·파일 선택" --> browser
browser -- "입력한 글자" --> api
api -- "탐지 결과·가린 글자" --> browser
api -. "규칙으로 가린 글만<br/>store: false" .-> openai
openai -. "찾은 이름" .-> api
| 쓰는 방법 | 원문이 가는 곳 | 근거 |
|---|---|---|
| CLI · Python 라이브러리 | 이 PC 안 | 탐지와 마스킹이 로컬 프로세스에서 끝난다. core에서 네트워크를 쓰는 곳은 아래 --llm 한 곳뿐이다 |
--llm (이름 하이브리드) |
이 PC의 Ollama | name_llm.py의 _require_local_host가 localhost·127.0.0.1·::1이 아닌 주소를 거부하고, 요청은 프록시·리다이렉트를 거치지 않는다. Ollama가 요청을 ollama.com으로 넘기는 클라우드 모델(이름이 cloud로 끝나거나, Ollama 모델 정보에 원격으로 표시된 모델)은 원문을 보내기 전에 거부한다(#468). CLI와 MCP에는 이 주소를 바꾸는 옵션이 없다 |
| 데스크톱 앱 | 이 PC 안 | 로컬 CLI를 먼저 쓰고, 없으면 이 PC의 API(127.0.0.1:8000)로 넘어간다 (apps/desktop/lib/services/default_backend.dart) |
MCP anonymize_file |
이 PC 안 | 에이전트는 파일 경로만 넘기고 경로와 건수만 돌려받는다 |
MCP anonymize_text · scan_text |
이미 에이전트에 있음 | 에이전트가 가진 글자를 넘기는 도구라 그 원문은 에이전트가 이미 봤다. 가린 결과를 다른 곳으로 보내기 전에 쓴다. scan_text는 원문 값 없이 종류·위치·확신도·탐지기 이름만 돌려준다 |
| 웹 데모 (기본, 규칙 모드) | 우리 API 서버 (Vercel) | 파일은 브라우저에서 글자만 뽑고(apps/web/src/lib/extractText.ts) 그 글자를 서버로 보낸다. 규칙 모드에서 서버는 규칙 탐지기만 쓰고 LLM을 부르지 않는다. apps/api에는 입력을 로그·파일에 남기는 코드가 없고, 검증 오류 응답에서도 입력값을 지운다(apps/api/maskingtape_api/errors.py). 호스팅 플랫폼(Vercel)의 요청 기록은 우리 코드 밖이다 |
| 웹 데모 하이브리드 모드 (선택) | 우리 API 서버 → OpenAI API (상용 API) | 요청에 mode: "hybrid"를 고를 때만 쓴다. 서버는 규칙 결과로 먼저 가린 글(LabelAnonymizer)만 OpenAI 이름 판단기에 보내고(apps/api/maskingtape_api/services/core_adapter.py), 요청에 store: false를 넣으며 리다이렉트를 따라가지 않는다(services/openai_name_judge.py). 입력이 5,000자를 넘거나 같은 IP에서 60초에 10번을 넘게 요청하면 OpenAI를 부르지 않는다(apps/api/maskingtape_api/routers/pii.py, 서버 인스턴스마다 따로 세는 제한). 키는 서버 환경변수에만 있고, 키가 없거나 제한에 걸리거나 판단기가 실패하면 규칙 결과를 돌려주며 hybrid_failed로 알린다. 규칙이 놓친 개인정보는 가린 글에 그대로 남아 OpenAI로 함께 전송된다. 웹 화면에서는 입력창 아래 「탐지 방식」에서 「하이브리드 (OpenAI)」를 고를 때만 이 모드가 된다(기본은 규칙 전용, #705·#715) |
에이전트에게 원문 대신 가린 사본만 넘기려면
anonymize_file을 쓴다. 다만 에이전트가 자기 도구로
원본 파일을 여는 것까지 막지는 못하고, 탐지하지 못한 값은 사본에도 남는다. 실제 개인정보가 든
문서는 웹 데모 대신 로컬에서 도는 CLI·데스크톱·MCP로 처리한다.웹 데모의 하이브리드 모드는 이 프로젝트에서 상용 AI API를 부르는 유일한 곳이다. 라이브러리·CLI·MCP 서버·
데스크톱 앱은 상용 API 없이 완전히 동작하고, 운영사무국이 이 선택 옵션을 서면으로 확인해 주었다(2026-09-30).
코드 밖 운영 설정으로 OpenAI 프로젝트에 월 사용 한도를 걸어 두었고, 심사용 시연과 발표는 이 옵션을 쓰지 않고
로컬 구성(규칙 + Ollama)으로 한다. 규칙은 CLAUDE.md §2 3번에 있다.
MCP 서버로 쓰기 (AI 에이전트용)
에이전트가 한국어 데이터를 외부로 보내기 전에 자동으로 비식별화하는 프라이버시 계층:
pip install -e packages/core -e packages/mcp-server
claude mcp add maskingtape -- maskingtape-mcp # Claude Code 등록
제공 도구: scan_text(탐지 리포트), anonymize_text(mask/label/pseudonym 비식별화), anonymize_file(로컬 파일을 통째로 비식별화해 사본 저장). 상세: packages/mcp-server
저장소 구조
모든 탐지·마스킹 로직은 packages/core 하나에 있고, 나머지는 그걸 감싸 쓴다 — 탐지기를 한 번 고치면 모든 표면(CLI·MCP·API·앱)이 함께 좋아진다.
flowchart TD
core["packages/core: 탐지·마스킹 엔진"]
core --> cli["CLI: maskingtape 명령"]
core --> mcp["packages/mcp-server: MCP 서버"]
core --> api["apps/api: REST API"]
api --> web["apps/web: 웹 플레이그라운드"]
api --> desktop["apps/desktop: 데스크톱 앱"]
bench["bench: 합성 벤치마크"] -.->|정확도 측정| core
웹은
apps/api를 거쳐 동작한다. 데스크톱은 로컬 CLI를 우선 쓰고, CLI가 없으면apps/api로 넘어간다.
로컬에서 웹을 띄울 때는 API 백엔드도 함께 띄워야 한다 — 아래 웹 플레이그라운드 로컬 실행 참고.
웹 플레이그라운드 로컬 실행
웹은 /api를 REST 백엔드로 프록시한다. 터미널 두 개가 필요하다.
# 터미널 1 — API 백엔드 (기본 포트 8000)
pip install -e "packages/core" -e "apps/api"
python -m uvicorn maskingtape_api.main:app --app-dir apps/api --port 8000
# 터미널 2 — 웹
cd apps/web && npm install && npm run dev # http://localhost:5173
다른 포트에 API를 띄웠다면 VITE_API_TARGET으로 알려준다 (예: VITE_API_TARGET=http://127.0.0.1:8001 npm run dev).
| 경로 | 내용 | 담당 |
|---|---|---|
packages/core/ |
Python 탐지·마스킹 엔진 + CLI (순수 로직) | @ChoHyeonChan |
packages/mcp-server/ |
MCP 서버 — core를 에이전트 도구로 노출 | @ChoHyeonChan |
apps/api/ |
FastAPI 백엔드 — 웹·데스크톱 공용 | @kitae13 |
apps/web/ |
웹 플레이그라운드 (탐지 하이라이트 데모) | @plana1470 · @imsoo0816 |
apps/desktop/ |
Flutter 데스크톱 앱 (드래그&드롭 배치 처리, Windows 전용) | @stayalive000 |
bench/ |
합성 벤치마크 데이터 + F1 정확도 리포트 | @seoyeon056 |
개발에 참여하기
- CONTRIBUTING.md — 협업 규칙 (이슈 → 브랜치 → PR → 머지). AI로 작업한다면 이 파일부터 읽히세요.
- STRUCTURE.md — 폴더 구조 규칙 (기능·도메인별로 나눕니다)
- ROADMAP.md — 개발 계획과 현재 정확도
- CLAUDE.md — 대회 규정에서 나온 필수 규칙 (위반 시 팀 전체 실격)
- 진행 상황: Issues · Milestones
- 의존성을 추가할 땐 같은 PR에서 SBOM.md를 갱신합니다.
- 새로 만드는 소스 파일 맨 위에는 저작권·라이선스 헤더 두 줄을 넣습니다 — 언어별 형식은 CONTRIBUTING.md의 「라이선스 규칙」에 있고, 빠지면 CI가 막습니다.
라이선스
Apache-2.0. 우리가 새로 쓴 소스 파일은 전부 이 라이선스로 공개하고, 파일 맨 위에
저작권·라이선스 헤더 두 줄을 넣는다(규칙은 CONTRIBUTING.md의 「라이선스 규칙」).
- SBOM.md — 쓰는 라이브러리와 AI 모델의 버전·라이선스·출처. 부록에 배포물별 전이
의존성과 모델 상세(태그·양자화·파일 해시)까지 적어 둔다. - THIRD_PARTY_NOTICES.md — 배포물에 코드가 함께 실리는 제3자
소프트웨어와, 저장소에 들어 있는 제3자 템플릿 파일의 출처·라이선스 고지문. - AUTHORS.md — 헤더에 적은 "The maskingtape Authors"가 누구인지.
LICENSE 파일은 저장소 루트와 배포 패키지 세 곳(packages/core, packages/mcp-server,apps/api)에 둔다.
Reviews (0)
Sign in to leave a review.
Leave a reviewNo results found