Victor

[연재] 사진 정리 봇 — 로컬 ML로 공짜·오프라인

연재 · 2026-08-16

사진 15장을 넣었더니 전부 「기타」였습니다

 

도구를 다 만들고, 시험 코드 53개가 전부 통과한 다음이었습니다. 이제 진짜 사진을 넣어볼 차례라 외장하드에서 15장을 골라 시험 삼아 돌렸어요. 캐논 RAW 파일도 몇 장 섞었습니다.

 

결과가 이랬습니다.

 

인물 0
음식 0
풍경 0
문서 0
기타 15

 

전부 기타였습니다.

 

프로그램은 안 죽었어요. 에러도 없고, 경고도 없고, 15장을 다 읽었고, 폴더도 정확히 만들었고, 리포트도 예쁘게 나왔습니다. 그냥 답이 쓸모없었을 뿐이에요.

 

이게 이 편에서 제일 하고 싶은 얘기입니다. 프로그램이 멈추면 누구나 알아챕니다. 그런데 멈추지 않고 틀리는 것은 아무도 안 알려줘요. 시험 코드도 안 알려줍니다. 53개가 전부 초록불이었으니까요. 화면에는 "완료. 대표 15장" 이라고 뜹니다.

 

원인은 좀 허무했어요. 분류 후보 목록에 「기타」가 들어가 있었습니다.

 

CLIP_LABELS = {
    "인물": "a photo of a person, people, a portrait, a selfie",
    "음식": "a photo of food, a meal, a dish, a plate of food",
    "풍경": "a landscape photo, scenery, nature, an outdoor view",
    "문서": "a scanned document, a page of text, a receipt, a whiteboard",
    "기타": "a random everyday photo",
}

 

맨 아래 줄을 보세요. 「기타」의 설명이 "아무 일상 사진" 입니다. 그런데 세상 모든 사진은 아무 일상 사진이에요. 인물 사진도 일상 사진이고, 밥 사진도 일상 사진입니다. 그러니까 이 후보는 다섯 개 중에 늘 조금씩 이기는 만능 답이었던 겁니다.

 

고친 방법은 「기타」를 후보에서 빼는 것이었어요. 지금 설정은 이렇습니다.

 

CLASSIFY_CATEGORIES = ["인물", "음식", "풍경", "문서"]  # CLIP이 실제로 점수 매기는 카테고리
CLIP_CONFIDENCE_THRESHOLD = 0.5    # 최고 확률이 이 미만이면 '기타'

 

네 개만 경쟁시킵니다. 그리고 네 개 중 제일 높은 확률이 0.5를 못 넘으면 그때 「기타」로 떨어뜨려요. 「기타」는 이제 후보가 아니라 "모르겠다"는 뜻입니다. 점수 매기는 방식도 바꿨어요. 예전엔 유사도 숫자를 그대로 임계값과 비교했는데, 지금은 네 후보의 점수를 확률로 바꿔서 비교합니다. "이 사진이 음식일 확률 0.83" 처럼 읽히게요.

 

바꾸고 다시 돌렸더니 인물·음식·풍경·문서·기타·스크린샷으로 제대로 갈렸습니다. 커밋 시각이 7월 11일 밤 9시 10분이에요.

 

 

이 도구만 사이트에 안 올립니다

 

지금까지 이 연재에 나온 도구는 전부 웹이었습니다. 육아휴직 계산기, 카드 혜택, 지분 관계도, 성경 지도, 기술사 스터디. 만들었으니 올려서 남도 쓰게 하자는 쪽이었어요.

 

이건 반대입니다. 사이트에 안 올라가요. 제 맥에서만 돌고 끝납니다.

 

이유는 하나예요. 사진은 남한테 못 맡기는 자료입니다.

 

요즘 "AI로 사진 분류" 하면 보통 이렇게 합니다. 클라우드 회사의 비전 API에 사진을 올리고, 이게 뭐냐고 물어보고, 답을 받아서 폴더에 넣습니다. 코드도 짧고 정확도도 좋아요.

 

그런데 그 말은 제 사진 2만 6천 장이 남의 서버에 한 번씩 올라간다는 뜻입니다. 가족 사진이 거기 들어 있어요. 애 얼굴이 있고, 집 안이 찍혀 있고, 병원 서류 사진도 있습니다. 그걸 "분류해 줘" 하면서 통째로 올리는 게 맞나 — 여기서 멈췄습니다.

 

그래서 설계서 첫 표에 이렇게 못을 박았어요.

 

분류 엔진 | CLIP 로컬 ML(제로샷) + 메타데이터 | API키·비용·외부전송 없음

 

CLIP이라는 게 뭐냐면, 이미지와 문장을 같은 자로 재는 모델입니다. "a photo of food" 라는 문장과 사진 한 장을 넣으면 둘이 얼마나 닮았는지 숫자를 뱉어요. 학습을 새로 시킬 필요가 없습니다. 이미 배워서 나온 모델이라 라벨만 바꿔 끼우면 됩니다. 그리고 이걸 통째로 제 컴퓨터에 내려받아 돌릴 수 있어요.

 

첫 실행 때 모델 파일 400MB 정도를 한 번 받습니다. 그다음부터는 랜선을 뽑아도 돌아가요. API 키가 한 개도 안 들어갑니다.

 

여기서 하나 짚고 싶은 게 있어요. 공짜인 건 결과지 목표가 아니었습니다.

 

설계서에 이런 계산이 적혀 있긴 합니다.

 

LLM 비전 API(Haiku, 1만 장 기준 5천~2만 원)와 달리 반복 실행에도 비용이 들지 않는다.

 

2만 6천 장이면 한 번 돌릴 때마다 몇만 원이고, 고쳐서 다시 돌리면 또 몇만 원입니다. 실제로 저는 이 도구를 버그 잡느라 전체 재실행을 여러 번 했어요. 요금이 붙었으면 그 재실행을 못 했을 겁니다. 그러니까 돈이 안 드는 게 도움이 되긴 했는데, 애초에 로컬로 간 이유는 요금이 아니라 사진이 나가는 게 싫어서였습니다.

 

 

원본은 안 건드립니다 — 옮기지 않고 복사만

 

두 번째 규칙은 더 단순합니다.

 

원본 파일을 수정·이동·삭제하지 않는다. 복사 전용.

 

설계서에도, 계획서에도, 똑같은 문장이 그대로 옮겨 적혀 있습니다. 정리 도구인데 정리를 안 해요. 원본 폴더는 그대로 두고, 옆에 새 폴더를 만들어서 복사본을 예쁘게 늘어놓습니다.

 

당연히 용량이 두 배로 듭니다. 사진이 77GB니까 정리본까지 하면 150GB 가까이 먹어요. 비효율적이죠.

 

그래도 이렇게 한 이유는, 사진은 되돌릴 방법이 없기 때문입니다.

 

정리 도구가 판단을 잘못하면 어떻게 되나요. 중복이 아닌 걸 중복이라고 보고 지웁니다. 그러면 그 사진은 끝이에요. 휴지통에도 안 남는 경우가 있고, 외장하드는 더 그렇습니다. 다시 찍을 수 있는 사진이면 모르겠는데, 사진은 대부분 다시 못 찍어요.

 

그래서 편의를 버리고 되돌릴 수 있음을 골랐습니다. 잘못 정리했으면 정리본 폴더를 통째로 지우고 다시 돌리면 됩니다. 원본은 애초에 안 만졌으니까요.

 

같은 생각으로 기본 동작도 미리보기로 뒀어요. 그냥 실행하면 파일을 하나도 안 만들고 계획만 세웁니다. 진짜로 복사하려면 --execute 라고 따로 적어야 해요.

 

python run.py "/Volumes/T7 Shield/백업/1. 사진/사진" --dry-run    # 기본
python run.py "/Volumes/T7 Shield/백업/1. 사진/사진" --execute    # 이때만 진짜 복사

 

그리고 모르는 파일은 아예 안 건드립니다. 외장하드를 스캔해 보니 사진 말고도 별게 다 있었어요. pdf 29개, hwp 8개, docx 6개, zip 10개, apk 10개, m4a 4개. 이런 건 손대지 않고 리포트에 목록만 뜹니다. "여기 이런 것도 있더라"까지만 알려주고 끝이에요.

 

동영상은 중간쯤입니다. mp4 66개, mov 18개는 별도 폴더로 날짜별 정리만 하고, 내용 분류는 안 합니다. 영상 안을 들여다보는 건 이번 판 범위 밖으로 뺐어요.

 

 

날짜로 정리하는 도구가 날짜를 틀리고 있었습니다

 

이 도구가 하는 일 중에 제일 기본이 찍은 날짜로 폴더를 나누는 것입니다. 2024/2024-03/음식/ 이런 식이에요.

 

그런데 만든 지 한 시간도 안 돼서 그 날짜가 틀렸습니다.

 

사진 파일 안에는 EXIF라는 게 붙어 있어요. 카메라 이름, 조리개, 셔터 속도, 그리고 찍은 시각이 들어갑니다. 그 시각을 읽는 코드를 이렇게 짰었습니다.

 

exif = im.getexif()
raw = exif.get(36867)     # DateTimeOriginal

 

숫자 36867이 "찍은 시각" 칸의 번호예요. 그런데 이렇게 읽으면 아무것도 안 나옵니다.

 

EXIF가 한 겹이 아니거든요. 겉에 기본 정보가 있고, 그 안에 서랍이 하나 더 있습니다. 카메라 이름은 겉에 있고, 찍은 시각은 안쪽 서랍에 들어 있어요. 겉만 뒤지면 없는 게 당연했습니다.

 

지금 코드는 이렇습니다.

 

exif = im.getexif()
has_camera = bool(exif.get(_MAKE) or exif.get(_MODEL))
sub = exif.get_ifd(ExifTags.IFD.Exif)     # 안쪽 서랍을 연다
raw = sub.get(_DATETIME_ORIGINAL)

 

한 줄이 늘었어요. 그런데 이 한 줄이 없으면 어떻게 되냐면, 찍은 날짜를 못 찾으니까 파일 수정 시각으로 대신 갑니다. 그리고 파일 수정 시각은 백업하면서 바뀌는 값이에요. 2015년에 찍은 사진이 2023년 폴더에 얌전히 들어가 있게 됩니다.

 

이것도 프로그램이 안 죽는 종류의 오류였습니다. 폴더도 잘 만들어지고 파일명도 규칙대로 붙어요. 다만 전부 엉뚱한 해에 들어갈 뿐입니다.

 

고친 김에 회귀 시험을 하나 박아뒀어요. 안쪽 서랍에 날짜를 넣은 시험용 사진을 만들고, 그걸 제대로 읽어내는지 확인합니다. 같은 실수를 다시 하면 시험이 빨간불로 잡아줘요.

 

 

RAW 744장이 152장으로 줄어 있었습니다

 

여기가 이 편에서 제일 아찔했던 대목입니다.

 

전체 2만 6천 장을 처음으로 진짜 돌렸어요. 몇 시간 걸려서 다 끝나고 리포트를 열었는데, 캐논 RAW 파일 숫자가 눈에 걸렸습니다.

 

  • 스캔한 CR2 파일: 744장
  • 정리본에 남은 CR2: 152장

 

592장이 사라졌습니다.

 

정확히는 "사라진" 게 아니라 "쟤는 중복이니까 안 옮겨도 된다"고 판정된 겁니다. 원본은 안 건드리는 도구니까 실제로 없어지진 않았어요. 그래도 정리본만 보고 원본을 지웠다면 592장이 날아갔을 겁니다.

 

왜 그랬냐면요.

 

중복을 찾는 방법이 두 가지입니다. 하나는 파일 내용을 그대로 비교하는 것 — 이건 틀릴 일이 없어요. 바이트가 똑같으면 똑같은 파일입니다. 다른 하나는 비슷한 사진을 찾는 겁니다. 리사이즈했거나 다시 압축한 같은 사진을 잡으려고요. 이건 사진을 아주 작게 줄여서 지문 같은 걸 만들고, 그 지문이 비슷하면 같은 사진으로 봅니다.

 

문제는 CR2를 여는 방법이었어요. CR2는 사실 TIFF라는 형식을 뒤집어쓰고 있습니다. 그래서 일반 이미지 라이브러리로 열면 열리긴 열려요. 다만 원본 사진이 아니라 안에 딸린 아주 작은 미리보기 이미지가 나옵니다.

 

그 미리보기가 얼마나 작냐면, 지문을 뜨면 서로 다른 사진들이 같은 지문으로 나올 만큼 작았습니다. 전혀 다른 592장이 한 덩어리로 묶여서 "얘네 다 같은 사진"이 된 거예요.

 

고친 건 세 줄입니다.

 

def perceptual_hash(path: Path):
    if path.suffix.lower() in config.RAW_EXTS:
        # RAW(CR2 등)는 TIFF 기반이라 PIL로 열면 저해상 미리보기가 나와 서로 다른 사진의
        # phash가 충돌한다 → 유사중복 오병합. RAW는 정확중복(sha256)만으로 판정한다.
        return None

 

RAW는 "비슷한 사진" 검사에서 아예 뺐습니다. 내용이 완전히 똑같은 경우만 중복으로 봐요. 이러면 비슷한 RAW 두 장이 둘 다 남습니다. 조금 손해죠. 그런데 고유한 사진을 잃는 것보다는 낫습니다. 다시 돌렸더니 744장이 전부 대표로 살아남았어요.

 

그리고 여기서 사람이 한 일이 뭐였냐면 — 744와 152라는 두 숫자를 나란히 놓고 이상하다고 본 것뿐입니다. 프로그램은 정상 종료했어요. 리포트도 정상입니다. "중복 592장 제외" 라고 적혀 있었고, 그게 원래 중복을 잡으라고 만든 기능이니까 오히려 일을 잘한 것처럼 보였습니다.

 

잘못 만들어진 정리본 폴더는 통째로 지우고 처음부터 다시 돌렸습니다.

 

 

2만 6천 장에서 안 끝났습니다

 

시험용 사진 몇 장으로는 안 보이다가 실제 규모에서만 터지는 게 있어요. 속도가 그렇습니다.

 

"비슷한 사진 찾기"를 처음엔 이렇게 짰습니다. 전부를 전부와 비교하는 거예요. 1번을 2번부터 끝까지 비교하고, 2번을 3번부터 끝까지 비교하고… 사진이 100장이면 5천 번쯤 비교합니다. 별거 아니죠.

 

2만 6천 장이면 3억 번이 넘습니다.

 

만드는 도중에 이미 알고는 있었어요. 작업 원장에 이렇게 적어놨습니다.

 

Minor/Important(기록·최종검토): dedup reps간 O(n²) phash 비교 — 26k에서 수분~수십분.
CLIP이 더 오래걸려 우선순위 낮음. 필요시 BK-tree/버킷팅.

 

"알고는 있는데 지금은 다른 게 더 느리니까 나중에" 라고 적어둔 겁니다. 그리고 실제로 나중이 왔어요. 다른 걸 다 빠르게 만들고 나니까 이게 제일 느린 구간이 됐거든요.

 

고치면서 조건을 하나 걸었습니다. 빨라지되 결과가 달라지면 안 된다.

 

이게 왜 중요하냐면요. 속도를 고치는 일은 대개 "덜 비교하기"입니다. 덜 비교하면 못 잡는 게 생겨요. 그러면 중복이 안 잡히거나, 반대로 엉뚱하게 묶입니다. 그런데 빨라진 건 눈에 보이고, 다르게 묶인 건 눈에 안 보여요. 시간은 화면에 숫자로 뜨는데 묶임이 바뀐 건 2만 장 중에 어디가 바뀌었는지 알 수가 없습니다.

 

그래서 이렇게 확인했어요. 실제 사진 2,500장을 골라서, 옛날 방식(전부 대 전부)과 새 방식을 둘 다 돌리고 결과를 비교했습니다. 어떤 사진이 어떤 사진과 같은 그룹인지가 하나도 안 틀려야 통과입니다.

 

결과는 완전히 일치했어요. 그러고 나서 바꿨습니다.

 

  • 2,500장에서 약 2.9배 빨라짐
  • 전체 2만 6천 장 기준 추정 5.1분 → 약 10초

 

새 방식이 뭐냐면 트리를 하나 세우는 겁니다. 지문끼리의 거리에 규칙이 있어서, "이 사진과 거리 5 이내"를 찾을 때 트리의 대부분 가지를 아예 안 들어가도 된다는 걸 미리 알 수 있어요. 그래서 3억 번을 다 안 해도 됩니다. 묶이는 결과는 똑같고요.

 

 

빨라졌다가, 정리하다가, 다시 느려졌습니다

 

속도 얘기가 하나 더 있는데 이건 좀 웃깁니다.

 

분류 자체도 느렸어요. 사진 한 장씩 모델에 넣으니까 초당 13장이 나왔습니다. 2만 6천 장이면 34분이에요.

 

그래서 두 가지를 바꿨습니다. 하나는 한 장씩 말고 64장씩 묶어서 넣는 것. 다른 하나는 맥의 그래픽 칩(M4)에 일을 시키는 것. 파이썬 쪽에서 몇 줄로 켤 수 있는 기능이에요.

 

초당 86장이 됐습니다. 6.5배쯤이고, 34분짜리가 5분으로 줄었어요.

 

그런데 그다음이 문제였습니다. 배치로 넣는 코드가 좀 지저분해져서, 읽기 좋게 정리를 한 번 했어요. "묶어서 넣고 결과를 원래 순서로 돌려놓는" 부분을 따로 떼어내는 작업이었습니다. 정리하면서 사진을 미리 읽어들이는 부분의 병렬 처리를 같이 지웠어요. 단순한 게 낫다고 생각해서요.

 

그리고 다시 재보니 이랬습니다.

 

실측(실제 하드웨어)으로 76 img/s → 27 img/s 하락

 

3분의 1로 떨어졌습니다.

 

이유는 이랬어요. 그래픽 칩은 사진 64장을 한 번에 씹어먹는데, 그 64장을 파일에서 읽어서 크기 맞추는 일은 일반 CPU가 합니다. 앞에서 한 장씩 느긋하게 읽어주면 뒤에 있는 칩은 놀고 있는 거예요. 밥은 빨리 먹는데 반찬을 한 젓가락씩 갖다주는 상황입니다.

 

그래서 병렬 처리를 도로 살렸습니다. 이번엔 시험을 하나 더 붙여서요 — 여러 갈래로 나눠 읽어도 순서가 안 섞이고, 중간에 한 장이 깨져도 그 한 장만 빠지는지 확인하는 시험입니다.

 

이 대목을 굳이 쓰는 이유는, 코드를 깔끔하게 만드는 일이 성능을 깨뜨릴 수 있다는 게 저한테는 의외였기 때문이에요. 정리는 좋은 일이잖아요. 그런데 정리하다가 지운 그 몇 줄이 왜 거기 있었는지는 코드만 봐서는 안 보입니다. 재봐야 압니다.

 

 

파일 하나가 깨져서 전체가 멈추던 것

 

2만 장을 돌리는 도구에는 이 문제가 반드시 옵니다. 어딘가에 깨진 파일이 하나 있어요.

 

처음 코드는 파일을 못 읽으면 그냥 터졌습니다. 3시간짜리 작업이 2시간 40분쯤에서 파일 하나 때문에 멈추는 거예요. 그리고 어느 파일인지 찾아서 빼고 다시 처음부터 돌려야 합니다.

 

그래서 세 군데를 막았어요.

 

하나, 파일 내용 읽기. 못 읽으면 그 파일만 "읽을 수 없음" 표시를 달고 넘어갑니다. 표시가 파일마다 다르게 붙어서, 결과적으로 자기 혼자 한 그룹이 돼요. 다른 사진과 중복으로 안 묶이고 그대로 복사됩니다. 못 읽었으니 판단을 못 하겠다 → 그러면 안전한 쪽으로 둔다는 뜻입니다.

 

둘, 분류. 사진 64장 묶음을 넣었는데 그중 하나가 이상하면, 예전엔 그 묶음 64장이 전부 「기타」로 떨어졌습니다. 더 나쁜 판이었을 땐 그날 실행의 대표 사진 분류가 통째로 날아가는 구조였어요. 지금은 한 장씩 따로 감싸서, 깨진 한 장만 「기타」가 되고 나머지 63장은 멀쩡히 분류됩니다.

 

셋, 복사. 복사가 실패하면 그 항목만 오류 목록에 적고 다음으로 갑니다. 리포트 맨 아래에 실패 목록이 나와요.

 

원칙은 하나입니다. 한 장 때문에 전체가 죽지 않는다. 대신 무슨 일이 있었는지는 리포트에 반드시 남깁니다. 조용히 넘어가면 그게 더 나빠요.

 

 

스크린샷을 너무 많이 잡았습니다

 

폰 스크린샷은 따로 빼고 싶었어요. 사진첩에 제일 많이 쌓이는데 나중에 볼 일은 거의 없는 것들이니까요.

 

어떻게 구분하냐면 세 가지를 봅니다. 파일명에 screenshot 이나 스크린샷 이 들어 있나, EXIF에 카메라 정보가 없나, 그리고 화면 비율이 폰 화면 비율인가.

 

처음엔 비율 목록을 넉넉하게 잡았습니다. 16:9, 4:3, 16:10 같은 흔한 비율을 다 넣었어요.

 

그랬더니 진짜 사진이 스크린샷 폴더로 갔습니다. 당연하죠. 4:3은 카메라 사진의 기본 비율이에요. 3:2도 그렇고요.

 

지금은 이렇게 좁혔습니다.

 

# 스크린샷 판별용 화면비: 카메라 사진에 거의 없는 '세로로 긴 폰 화면' 비율만 사용해 실사진 오검출을 줄인다.
# (4:3, 16:9 등 흔한 사진 비율은 의도적으로 제외 — 이름 패턴으로만 잡는다.)
_SCREEN_RATIOS = [9 / 16, 9 / 18, 9 / 19.5, 9 / 20, 9 / 21]

 

세로로 아주 긴 것만 봅니다. 요즘 폰 화면처럼요. 가로 사진은 비율로는 아예 안 잡고, 파일명에 스크린샷이라고 적혀 있을 때만 잡아요.

 

이건 일부러 덜 잡는 쪽으로 튼 겁니다. 스크린샷 몇 장이 사진 폴더에 섞여 있는 건 눈으로 보면 바로 알고 그냥 지우면 돼요. 그런데 진짜 사진이 스크린샷 폴더에 파묻히면 찾을 생각을 안 하게 됩니다. 두 방향의 실수가 값이 다릅니다. 어느 쪽으로 틀리는 게 덜 아픈지를 정하는 건 기계가 못 해요.

 

같은 정리를 하면서 하나 더 지웠습니다. 도움말에 --faces 라는 옵션이 떠 있었는데, 아무 일도 안 하는 옵션이었어요. 얼굴 검출을 나중에 붙이려고 이름만 만들어둔 거였습니다. 붙일 계획이 없어졌으면 이름도 지우는 게 맞아요. 있는데 아무 일도 안 하는 스위치가 제일 나쁩니다.

 

 

손으로 골라 모은 폴더에서는 중복 제거가 재앙이었습니다

 

여기서 이 도구가 한 번 더 방향을 틉니다.

 

외장하드에는 정리 안 된 사진 더미만 있는 게 아니었어요. 제가 손으로 주제별로 나눠둔 폴더가 아홉 개 있었습니다. 오래전에 시간 들여 골라 담은 것들이에요.

 

이건 내용 분류가 필요 없습니다. 이미 주제로 나뉘어 있으니까요. 필요한 건 각 폴더 안에서 월별로만 정리하는 거였어요. 그래서 --by-date 라는 모드를 하나 더 붙였습니다. 분류를 아예 건너뛰고 주제/연-월/ 로만 담아요.

 

그리고 첫 실행에서 사고가 났습니다.

 

한 폴더에 사진이 405장 있었는데, 214장이 중복으로 빠졌습니다. 전부 고유한 사진이었어요.

 

이건 버그가 아닙니다. 기능이 제대로 동작한 결과예요. 손으로 고른 사진들은 서로 닮았거든요. 같은 자리에서 연달아 찍고, 그중 제일 나은 걸 남긴 게 아니라 괜찮은 걸 여러 장 남긴 폴더니까요. "비슷한 사진 찾기"에게 그건 중복 덩어리입니다.

 

그래서 모드에 따라 판정을 다르게 했어요.

 

def group_duplicates(photos, workers: int = 1, near_dup: bool = True) -> None:

 

near_dup 이라는 스위치를 하나 만들고, --by-date 로 돌 때는 이걸 끕니다. 내용이 완전히 똑같은 파일만 중복으로 보고, 비슷한 사진은 전부 남겨요.

 

이게 왜 사람 몫이었냐면요. 프로그램 입장에서 정리 안 된 폴더와 골라 모은 폴더는 똑같은 사진 뭉치입니다. 그 둘이 다르다는 건 거기 사진을 담은 사람만 압니다. 한쪽은 "비슷한 건 하나만 남겨줘"가 맞고, 다른 쪽은 "비슷하다고 지우면 안 돼"가 맞아요. 같은 기능이 한쪽에선 정리고 한쪽에선 파괴입니다.

 

잘못 만들어진 폴더를 지우는 데도 한 번 걸렸습니다. 외장하드가 exFAT이라 파일은 지워지는데 빈 폴더가 남더라고요. 폴더 구조만 앙상하게 남은 걸 다시 치우고 재실행했습니다.

 

 

두 시간 반짜리 도구, 그리고 남은 솔직한 얘기

 

기록을 보면 이렇습니다.

 

  • 7월 11일 20:00 첫 커밋(설계서·계획서)
  • 20:53 파이프라인 조립 완료 — 스캔부터 리포트까지 다 돌아감
  • 21:10 "전부 기타" 발견하고 수정
  • 21:30~21:48 그래픽 칩 배치 처리, 병렬 복원, 트리 최적화
  • 22:15 RAW 오병합 수정
  • 다음 날 08:32 월별 모드 추가, 16:43 골라 모은 폴더 대응

 

커밋 28개, 시험 코드는 53개에서 시작해 77개로 늘었습니다. 뼈대를 만드는 데는 한 시간이 안 걸렸고, 나머지 시간은 전부 "돌려봤더니 틀렸다"를 고치는 데 썼어요.

 

이게 이 편의 모양입니다. feat: 로 시작하는 커밋보다 fix: 로 시작하는 커밋에 이야기가 훨씬 많아요.

 

솔직하게 남길 것도 있습니다.

 

하나, 일회성 도구입니다. 다시 돌리면 이미 있는 파일은 건너뛰게 만들어놨는데, 사진을 새로 추가하고 다시 돌리면 파일명 순번이 밀려서 같은 사진이 다른 이름으로 또 복사될 수 있어요. 한 번 크게 정리하는 데는 맞고, 매달 돌리는 데는 안 맞습니다. 이건 아직 안 고쳤어요.

 

둘, 분류 정확도를 세어보지 않았습니다. "제대로 갈리더라"는 눈으로 본 인상이지 숫자가 아니에요. 인물 몇 %가 맞았는지 저는 모릅니다. 2만 장을 사람이 다 확인할 수가 없어서요. 다만 틀려도 손해가 작게 설계했습니다 — 폴더가 좀 틀릴 뿐이고, 원본은 그대로 있으니까요.

 

셋, 전체 한 번 도는 데 몇 시간 걸립니다. 빠르게 만들었다고 썼지만 그건 분류 단계 얘기고, 77GB를 전부 읽어서 지문 뜨는 시간은 그대로예요. 저녁에 걸어놓고 자는 종류의 작업입니다.

 

 

그래서 사람이 어디에 남았나

 

기계가 한 것. 파이프라인 일곱 단계 설계와 구현, 데이터 모델, EXIF 파싱, 해시 두 종류, 트리 자료구조, 배치 추론, HTML 리포트, 시험 코드 77개. 두 시간 반 만에요. 저 혼자였으면 이 중 절반도 못 만들었습니다.

 

사람이 한 것은 다섯 가지입니다.

 

첫째, 클라우드를 안 쓰기로 한 것. 이건 코드가 아니라 방향입니다. API를 쓰면 코드가 짧아지고 정확도도 좋아져요. 그런데 그 대가로 가족 사진 2만 6천 장이 나갑니다. 이 도구가 이 연재에서 유일하게 사이트에 안 올라가는 이유도 같아요. 뭘 남한테 맡길 수 있고 뭘 못 맡기는지는 그 자료의 주인만 압니다.

 

둘째, "전부 기타"를 이상하다고 본 것. 프로그램은 안 죽었고 시험은 다 통과했고 리포트도 정상이었습니다. 답만 쓸모없었어요. 이건 실행 결과를 눈으로 보고 "이게 말이 되나" 물어야만 나옵니다.

 

셋째, 744와 152를 나란히 놓고 본 것. RAW가 592장 사라진 것도 프로그램 입장에선 정상 작동이었습니다. "중복 제거 592장"은 잘 돌아간 것처럼 보이는 숫자예요. 들어간 개수와 나온 개수를 대조한 게 사람이었습니다.

 

넷째, 속도를 고치면서 결과가 바뀌면 안 된다는 조건을 건 것. 빨라진 건 숫자로 뜨고 다르게 묶인 건 안 뜹니다. 그래서 옛날 방식과 새 방식을 둘 다 돌려서 대조했어요. 빨라진 대신 다르게 묶이면 그건 고친 게 아닙니다.

 

다섯째, 복사 전용을 규칙으로 박은 것. 용량이 두 배 드는 걸 알면서요. 정리 도구가 판단을 잘못했을 때 어디까지 망가지게 둘 것인가 — 이걸 만들기 전에 정한 게 결국 제일 크게 남았습니다. 실제로 이 도구는 만드는 이틀 동안 두 번이나 잘못된 정리본을 만들었어요. 둘 다 폴더를 지우고 다시 돌리면 끝이었습니다. 원본을 옮기는 도구였으면 그 두 번이 회복 불가능한 사고였을 겁니다.

 

마지막 게 제일 중요한 것 같아요. AI와 뭔가를 만들 때 우리는 보통 "잘 만드는 법"을 생각합니다. 그런데 이 도구에서 제일 값을 한 건 잘 만든 부분이 아니라, 틀렸을 때 되돌아올 자리를 미리 만들어둔 것이었습니다. 잘 만들 자신이 없어서 그렇게 한 건데, 결과적으로 그게 맞았어요.

 

 

다음 편

 

여기까지가 사진 정리 봇입니다. 이 연재에서 유일하게 웹이 아닌 도구, 유일하게 사이트에 안 올라가는 도구 얘기였어요.

 

다음 편은 집을 짓는 얘기입니다. 지금까지 만든 도구들이 전부 따로따로 떠 있었거든요. 계산기 주소 하나, 카드 주소 하나, 지분 주소 하나. 제가 뭘 만들었는지 저조차 한눈에 못 보는 상태였습니다.

 

그걸 한 곳으로 들이는 편이에요. 그리고 글을 올리는 것과 사람이 사는 사이트는 다르다는 걸 거기서 알게 됩니다.

 

#사진정리 #사진백업 #외장하드정리 #사진분류 #중복사진제거 #CLIP #로컬AI #오프라인AI #온디바이스 #개인정보보호 #프라이버시 #API키없이 #무료도구 #파이썬 #파이썬프로젝트 #CLI #EXIF #메타데이터 #RAW파일 #CR2 #캐논 #퍼셉추얼해시 #맥 #애플실리콘 #GPU가속 #원본보존 #드라이런 #AI활용 #클로드 #휴먼인더루프

댓글

    로그인 없이 쓰는 대신, 등록한 댓글은 직접 지울 수 없습니다. 지워야 할 댓글은 옆의 신고를 눌러주세요.
    네이버 블로그글 원문 보러가기 →유튜브 쇼츠만드는 장면 20초로 보기 →
    관리자 Victor · 문의는 우상단 신고·제안으로 남겨주세요.
    글과 화면은 정적 파일로 만듭니다. 댓글과 조회수만 외부 데이터베이스에 저장합니다 — 개인정보처리방침

    오류 신고 · 제안

    관리자 Victor에게 전달됩니다