뜯어본 것공개

watermarks-remover

AI 티를 지우는 도구를 검사 게이트 점검용으로 열었습니다. 그런데 AI를 잡으라고 만든 지표가, 사람이 쓴 제 글을 먼저 잡았습니다.

지운 것은 하나도 없고, 대신 제 배포 스크립트가 제 규칙을 어기고 있는 걸 찾았습니다. watermarks-remover 는 AI 생성물에 박히는 프로버넌스 표식을 걷어내는 파이썬 엔진입니다. 11일 만에 별 16,619개. 제가 이걸 연 이유는 표식을 지우려는 게 아니라 제 발행 파이프라인의 검사 게이트를 점검하려는 것이었습니다 — 배포 전 마커 스캔이 새는 곳이 있나, 규칙으로만 있고 검사기가 0인 「균일한 문장 리듬」을 결정론 코드로 만들 수 있나. 도구는 전면 기각했습니다. 제 발행 코퍼스 6.1MB 를 이 레포의 스캐너로 훑으니 표식이 0건이었기 때문입니다. 그런데 유일하게 탐났던 지표 하나를 제 글에 돌렸을 때 제일 값비싼 사실이 나왔습니다 — AI 를 잡으라고 만든 지표가, 사람이 쓴 제 글을 먼저 잡았습니다.

입력 하나가 갈라지는 세 갈래 — 라우터가 먼저 종류를 정한다 라우터 — 확장자 우선 · 매직바이트 차선으로 다섯 갈래 분류 어디에도 안 걸리면 unknown → 파일을 쓰지 않고 종료코드 2로 죽는다 (fail-closed) A. 보이지 않는 유니코드 제로폭 · 양방향 · 태그 · 비문자 몇 개 지웠는지 검산된다 B. 토큰 샘플링 워터마크 LLM 재작성으로 공격한다 스스로 검산 불가라고 규정 파일층 — C2PA · EXIF · XMP 이미지 · 문서 · 오디오 20여 포맷 지운 뒤 자기 출력을 다시 스캔 같은 엔진이 세 얼굴로 나온다 — 그리고 무거운 것은 아예 안 담는다 CLI — inspect · clean · audit 의존성 0 의 stdlib HTTP 서비스 에이전트 스킬 — 코드 0, curl 만 ⇒ 확실히 지운다고 말할 수 있는 건 A 와 파일층뿐이고, B 는 이 레포 자신이 검산 불가라고 적는다 픽셀 제거·점수 산출 같은 무거운 백엔드는 번들하지 않고 핀 커밋으로 외부에서 받는다 — 라이선스 격리가 이유다
포맷 20종을 세는 것으로는 이 물건이 안 보인다. 검산되는 층과 안 되는 층을 코드 경로부터 갈라 놓은 것이 정체다.

무엇을 하는 물건인가 — 「확실한 것」과 「최선노력」을 리포트에서 합산하지 않는다

AI 가 만든 파일에서 「이건 AI 가 만들었다」는 증거를 걷어내는 엔진입니다. 증거를 세 종류로 봅니다. 눈에 안 보이는 유니코드로 심어진 캐리어, 생성 시 토큰 선택 확률을 비틀어 심는 통계 워터마크, 파일 안에 구조로 박히는 프로버넌스 메타데이터.

설계의 중심은 이 셋의 성격이 다르다는 걸 인정한 데 있습니다. 유니코드는 결정론 코드로 지우고 몇 개 지웠는지 세서 돌려줍니다. 파일 메타데이터도 지운 뒤 자기 출력물을 다시 스캔해 잔존을 신고합니다. 반면 통계 워터마크는 외부 LLM 에게 글을 다시 쓰게 하는 것 말고 방법이 없고, 그건 검산이 불가능합니다. 레포 방침이 이렇게 적혀 있습니다 — “리포트는 검증 가능한 작업과 최선노력 작업을 반드시 분리해야 한다.” 이 한 줄이 여기서 제일 값나가는 문장입니다.

항목 실측 (2026-08-22)
규모 151 파일 · 약 1.13MB · 별 16,619 · 포크 1,902
엔진 / 테스트 service/scripts 14,141 LOC · 테스트 41파일 10,592 LOC · def test_ 549개
의존성 런타임 0 (Python 3.10+ stdlib). dev 4종만 정확 핀
라이선스 MIT — ⚠ 픽셀 계열 백엔드는 우산 밖(상류에 라이선스 없음 / 비상업 연구용)
활동 08-11 생성 → 08-21 마지막 push. 활동 기간 11일, 커밋 141, 릴리스 5회
기여 분포 27명이지만 오너가 63/122 — 버스 팩터 1

11일짜리 스프린트입니다. 별 16,619개는 채택이 아니라 바이럴이고, 장기 유지보수 이력은 아직 없습니다. CI 는 3-OS 매트릭스에 lint·의존성 감사·OpenAPI 스펙 검증까지 걸려 있어 나이에 비해 단단해 보이는데, 뒤에서 그 단단함이 무엇을 못 잡는지가 나옵니다.

대표 기법 — 값이 나가는 건 지우는 코드가 아니라 「모를 때 어떻게 하는가」다

기법 내용
fail-closed 라우팅 확장자에도 매직바이트에도 안 걸리는 입력은 예전엔 텍스트로 폴백했습니다. 지금은 파일을 쓰지 않고 종료코드 2로 죽습니다. “UTF-8 로 못 읽힌다” 만으로는 바이너리 판정을 하지 않아 다른 인코딩 텍스트는 살립니다
문맥 인지 스트립 같은 코드포인트라도 앞뒤를 보고 다르게 처리합니다 — ZWJ 는 이모지 사이에서만, 한글 필러는 같은 자모 뒤에서만 살립니다. 그리고 미할당 문자를 유니코드 카테고리로 잡지 않고 명시 레인지로만 잡습니다. 근거가 주석에 있습니다 — “Cn 규칙은 갓 할당된 진짜 문자를 파괴한다.” 실제로 그 사고를 겪고 사후 수리한 이력이 있습니다
PDF 2단 파이프라인 메타데이터 제거 도구가 PDF 를 증분 갱신합니다 — 종료코드 0, 뷰어엔 안 보이는데 원본 바이트는 남고 되돌릴 수 있습니다. 유일한 단서가 “파일이 오히려 커진다”입니다. 그래서 뒤에 재직렬화를 한 단계 붙여 참조 잃은 객체를 실제로 떨굽니다
청소 후 재검사 “지웠다” 로 끝내지 않고 산출물을 다시 검사에 태워 잔존을 돌려줍니다. 「청소 완료」와 「청소했지만 남았다」가 종료코드로 갈립니다
소견 신뢰도 4등급 모든 발견을 confirmed / probable / informational / likely_false_positive 로 매핑하고, 전체 바이트 스캔은 스스로 최하 등급으로 강등합니다 — “스캐너 히트는 신호이지 판정이 아니다.”
도구 버전 프로브 PATH 에 있다고 available 이라 하지 않고 실제로 --version 을 돌려 종료코드를 봅니다 — which 하나로는 틀린 질문에 답하게 된다.”

한 가지 더 있습니다 — 이 편의 주인공입니다. 워터마크가 아니라 「AI 냄새」를 통계로 채점하는 스코어러가 따로 붙어 있습니다. 세 항의 가중합입니다 — 상투구 사전 밀도 0.45, 문장 길이 변동계수 0.45, 어휘 다양도 0.10. 문장이 둘 미만이라 변동계수를 못 재면 그 항을 빼고 나머지로 재정규화합니다.

깨본 결과 — 지배적 실패는 오작동이 아니라 「거짓 무죄 판결」이다

확실히 지운다던 절반부터 흔들립니다. C2PA 2.x 표준에는 소프트 바인딩 해석 API 가 정식으로 들어가 있습니다. 파일에 박힌 매니페스트가 뜯겨도 비가시 워터마크나 지각 지문으로 원격 저장소를 조회해 프로버넌스를 되찾는 구조입니다. 이 레포가 지우는 것은 표준이 이미 「없어져도 되게」 설계해 둔 쪽이고, 레포도 소프트 바인딩은 범위 밖이라고 적습니다.

간판인 벤더 커버리지 네 칸 중 둘은 살아 있는 탐지 경로가 없습니다. Claude 의 표식은 공식 발표상 토큰 생성의 난수원을 바꾸는 통계 워터마크이지 유니코드 캐리어가 아닙니다. 즉 결정론적으로 검산되는 층은 그 표식을 아예 못 건드리고, 코드의 해당 탐지기는 로직 없는 예약 슬롯입니다. Gemini 칸은 레포가 스스로 사망 선고를 적어 뒀습니다 — 상류가 API 에서 텍스트 워터마킹을 철회했습니다.

효능 수치는 어디에도 없습니다. README 73,956바이트에 퍼센트 형태의 효능 수치가 0건, 벤치 폴더에 커밋된 실행 결과도 0건입니다. 그리고 오너 본인이 「결과를 낼 수 있게 만드는」 PR 을 두 번 올렸다가 두 번 미머지로 닫았습니다. 숫자가 아직 없는 게 아니라, 숫자를 낼 장치를 두 번 들였다가 두 번 물렀습니다.

반복 결함은 한 가지 모양입니다 — 못 지웠는데 깨끗하다고 인증합니다. 6일 만에 같은 모양의 이슈가 9건 열렸습니다. 잘린 오디오 태그를 소견 0건으로 보고하고 그 파일에 청결 인증을 찍고, 이미지 점수기가 죽었을 때와 워터마크가 없을 때가 똑같이 clean 으로 읽힙니다. 파싱 실패를 「없음」으로 접는 패턴이고, 사례별로 하나씩 잡고 있어 남은 포맷에서 또 나올 것으로 봐야 합니다. 그중 하나는 되돌릴 수 없습니다 — 제자리 수정을 두 번 돌리면 백업 파일을 청소본으로 덮어써 원본이 영구히 사라지는데, 같은 함수의 독스트링이 “원본은 절대 부분 손실되지 않는다” 고 약속하고 있습니다.

테스트 549개가 왜 이걸 못 잡느냐의 답이 트래커에 있습니다. “기여자들이 Claude 를 쓰는 건가요” 라는 질문에 오너가 그렇다고 답했습니다. 11일에 코드 14k + 테스트 10.6k LOC 입니다. 코드와 테스트가 같은 생성기의 같은 패스에서 나오면, 테스트는 생성기의 사각지대를 원리적으로 못 덮습니다. 커버리지 게이트 부재보다 이쪽이 본질입니다.

공정하게, 제 1차 판정 둘을 취소합니다. 광고된 동영상 경로가 파일을 손상시킨다던 이슈는 이미 고쳐져 있었습니다 — 삭제할 박스를 같은 크기의 0 채움 박스로 치환해 오프셋 테이블이 유효하게 남습니다. ⚠ 관련 문자열이 코드에 없다고 미수정으로 읽으면 오진합니다. 다른 하나는 이슈 제목의 SSRF 라는 이름을 그대로 옮긴 것이 과장이었습니다 — 엔드포인트가 공격자 입력이 아니라 운영자 환경변수라, 실제 결함은 내부망 피벗이 아니라 리다이렉트 시 인증 토큰과 본문이 새는 것입니다. 이슈 제목은 사실이 아니라 남의 판정입니다.

못 한 것. 이 레포의 벤치를 직접 못 돌렸습니다 — GPU 와 외부 체크아웃 4종이 필요하고 코퍼스 씨앗 8개가 전부 영어입니다. 그래서 “얼마나 지워지는가” 는 이 페이지에서도 미측정으로 남습니다. 워터마크가 실제로 박힌 파일에 대 본 적도 없습니다. 제 환경에 그런 파일이 없어서입니다.

내 환경에 대봤다 — 잡을 것이 0건이었다

레포 HEAD 의 스캐너를 그대로 임포트해 제 발행 코퍼스 전량에 돌렸습니다.

무엇을 쟀나 결과
보이지 않는 유니코드 — 문서 196편 + 정적 HTML 238장 + 원고 7편 (6.1MB) 0건
청소 함수 라운드트립 — 같은 196편, 기본·파라노이드 양쪽 196/196 바이트 동일 = 순수 no-op
발행 이미지 14장의 C2PA·AI 메타데이터 14/14 전부 없음 — 헤드리스 렌더 산출물이라 애초에 안 박힌다
로컬 뇌 파일까지 확대 (스킬 195편 + 메모리 180편) 5파일에 7개 — 전부 발행 경로 밖, 조치 불필요

표적이 없었습니다. 훼손 위험 0이면서 동시에 변경 0이면, 그건 게이트가 아니라 장식입니다.

유일하게 탐났던 AI 냄새 스코어러는 한국어에서 절반이 죽습니다. 상투구 패턴 24개 중 비ASCII 가 0개라 가중치 0.45짜리 항이 영원히 0입니다. 같은 성격의 대조군을 두 언어로 만들어 채점하니 영어 0.887(HIGH) 대 한국어 0.438(LOW) — 임계 0.65 는 한국어가 도달할 수 없는 값입니다(이론 천장 약 0.55). 그래서 언어 중립인 한 항만 떼서 재봤습니다.

문장 길이 변동계수(CV) — 낮을수록 리듬이 균일하다. 같은 계산기를 세 코퍼스에 돌렸다 사이트 장문 98편 중앙값 0.668 └ 임계 아래 2편 중 최저 0.230 짧은 원고 6편 최고 0.550 └ 이미 게시한 원고 최저 0.298 한국어 AI 대조군 0.055 임계 0.35 ⇒ 이미 게시까지 끝낸 사람 글 한 편이 임계 아래다 — 짧은 글에서 오탐 1/6 긴 글은 리듬이 흩어지고, 짧은 글은 사람이 써도 균일하다. 이 지표가 재는 것은 「누가 썼나」가 아니라 「얼마나 길게 썼나」다 그래서 임계 0.35 는 장문 전용으로 내렸다. 짧은 채널에 거는 배선은 철회했다 ⚠ 대조군은 제가 쓴 한 편이다 — 분리도의 절대값은 못 믿는다. 같은 계산기를 세 곳에 돌린 상대 비교만 성립한다
장문 98편에서는 임계 아래가 2편이었다. 실제 적용처인 짧은 원고로 다시 재니 1/6 이었다.

사이트 장문 98편은 중앙값 0.668 로 널찍하게 흩어져 있습니다. 임계 0.35 아래는 2편뿐이었고 둘 다 색인 성격의 짧은 페이지라, 1차에는 “오탐 2%” 로 적고 넘어갔습니다. 그런데 이 게이트를 실제로 걸려던 자리는 장문이 아니라 짧은 발행 원고였습니다. 그쪽 6편을 다시 재니 0.298~0.550 의 좁은 띠에 눌려 있었고, 그중 하나 — 제가 손으로 쓰고 게시까지 끝낸 글 — 이 0.298 로 임계 아래였습니다. 짧은 글은 사람이 써도 균일합니다. 이 지표가 재는 것은 「누가 썼나」가 아니라 「얼마나 길게 썼나」입니다.

수확 — 4종에서 1.5종으로

후보 판정 사유
문장 리듬 변동계수 한 항 채택(반쪽) 결정론 리듬 검사기가 제게 정말 0건이 맞습니다. 다만 장문 전용이고, 짧은 채널 임계는 별도 코퍼스로 다시 재기 전까지 만들지 않습니다
가중합 3항 골격 + 결측 항 재정규화 기각 3주 전에 제가 더 나은 형태로 만들어 뒀습니다 — 테스트케이스 파이프라인의 확신도 채점기에 규칙표·상한 클램프·선택 입력·임계 역산 스윕·기대표 잠금이 있고, “축이 다른 감점을 상쇄 금지” 같은 함정 수리가 주석에 박혀 있습니다
fail-closed 파일 선택 원칙만 남에게 배울 기법이 아니라 제 준수 결함 1건이었습니다. 아래
보이지 않는 유니코드 검출 세트 조건부 보류 이득이 실측 0 이라 보험으로만 정당화됩니다. host 도 이미 있습니다 — 기존 링크 검사기가 dist 전량을 훑고 눈에 보이는 글자만 뽑는 루프를 돌립니다. 신규 파일이 아니라 그 안 세 줄입니다

세 번째 줄이 이 조사의 실제 산출입니다. 제 전역 규칙 첫 줄이 “애매하거나 실패하면 하지 않는 쪽이 기본값” 이고, 배포 게이트의 라이브러리 헤더 주석은 자기 자신을 fail-closed 라고 규정합니다. 그런데 그 게이트가 파일을 고르는 방식은 확장자 화이트리스트 — 아는 확장자만 검사하는 fail-open 입니다. 지금 실누락은 0건이지만, .mjs·.rss 같은 게 산출물에 생기는 순간 경고 한 줄 없이 무검사로 통과합니다.

그리고 여기서 남의 구현은 들이지 않기로 했습니다. 레포는 매직바이트 28종 + NUL + 제어문자 비율로 판정하는데, 259 파일 규모에서 그 표는 유지보수 부채입니다. 같은 fail-closed 를 세 줄로 얻습니다 — 아는 목록 어디에도 없는 확장자가 나타나면 그냥 멈춥니다. 원칙은 채택, 구현은 기각.

부수적으로 — 제 수집기는 원본이 아니라 복제본을 후보로 올렸습니다

후보 목록에 Leutenegger/watermarks-remover 가 올라와 있었습니다. 피드에 실린 것은 guillaumemeyer/watermarks-remover ★16,619 입니다. 둘은 다른 저장소입니다.

원본 복제본
생성 2026-08-11 2026-08-19 (8일 뒤)
16,619 929 (3일 만에)
포크 플래그 false false — 깃허브 포크가 아니다
README 1,120줄 1,063줄 중 1,058줄이 원본과 일치 (99.5%)

복제본 README 안에 원저자 저장소를 가리키는 링크가 15개 그대로 남아 있습니다 — CI 배지가 남의 저장소를 가리킵니다. 포크 관계만 끊긴 통짜 복사본입니다. 제 후보 다리는 별 수와 낱말 규칙만 보니 원본과 복제를 구분하지 못합니다. 별이 빠르게 붙는 건 도구가 좋다는 신호가 아니라 화제가 붙었다는 신호이고, 복제본은 그 화제만 빌려 가면 됩니다.

판정

항목 판정
레포·서비스·스킬·도커 도입 기각. 파일 한 톨도 제 트리에 들이지 않습니다
파일 메타 / 이미지 / 문서 경로 기각. 필요해지면 기성 도구를 직접 부릅니다 — 그쪽은 제자리 수정을 안 해서 원본 소실이 구조적으로 불가능합니다
수확 1.5종 — 리듬 변동계수 한 항(장문 전용) + fail-closed 준수 복구
회사 파이프라인 반입 부적절. 기술 판정과 별개입니다

다시 여는 조건 넷 — ① 반기 1회 재스캔에서 표식이 처음으로 1건이라도 나올 때. ② 한국어에서 검증된 텍스트 워터마크 탐지기가 나올 때. 그 전까지 재작성 층은 검증 수단이 없어 판정 자체가 불가능합니다. ③ 데이터 손실·허위 clean 4종의 PR 이 머지되고 다음 릴리스가 나올 때. ⚠ 그때도 도구가 고쳐져도 표적이 없으면 여전히 기각이라 ①부터 다시 잽니다. ④ 이미지·PDF 를 외부에 배포하는 워크플로가 생길 때.

이 편에서 값을 치른 교훈은 도구가 아니라 순서입니다 — 검출기를 들이기 전에 자기 코퍼스로 오탐률부터 재라. 그것도 실제로 걸 자리의 코퍼스로 재야 합니다. 저는 사이트 장문에서 재고 「오탐 2%」라 적었는데, 그 규칙이 사는 채널로 옮겨 재니 1/6 이었고 걸린 것은 제가 쓴 글이었습니다.

그리고 하나 더. 별 16,619개짜리 레포를 뜯어서 남은 게 제 규칙 문서 한 줄을 제 배포 스크립트 한 줄이 어기고 있더라는 자기 대조 1건이었습니다. 남의 물건을 보는 값은 가져올 것에만 있지 않습니다 — 남의 원칙을 들고 제 코드를 보면, 제 원칙으로 볼 때는 안 보이던 것이 보입니다.