๐ธ ์ฌ์ง ์ ๋ฆฌ ์๋ํ ํ๋ก์ ํธ ๊ณํ v2
๐ธ ์ฌ์ง ์ ๋ฆฌ ์๋ํ ํ๋ก์ ํธ ๊ณํ v2
์น์ ์ ๋ชฉ: โ๐ธ ์ฌ์ง ์ ๋ฆฌ ์๋ํ ํ๋ก์ ํธ ๊ณํ v2โํ๋ก์ ํธ ๊ฐ์: NAS์ ๋ถ์ฐ ์ ์ฅ๋ ์ฌ์ง์ ์ค๋ณต ๊ฒ์ถ ๋ฐ ์๋ ์ ๋ฆฌ
์์ฑ์: ํ๋์ด + 8๊ฐ ๋ชจ๋ธ ํ๋ ๋ถ์
์์์ผ: 2026-04-21
์ํ: โณ ๊ณํ ์ค
0. ๋ชจ๋ธ ํ๋ ๋ถ์ ์์ฝ
์น์ ์ ๋ชฉ: โ0. ๋ชจ๋ธ ํ๋ ๋ถ์ ์์ฝโ๐ก glm-5.1, qwen3.5, minimax-m2.5, gemma4:31b, deepseek-r1, llama3.1 6๊ฐ ๋ชจ๋ธ์ด
๊ธฐ์กด ๊ณํ์ ๋ฆฌ๋ทฐํ๊ณ ๊ฐ์ ์์ ์ ์ํจ (moondream, gemma4:e2b์ ์ค์ ์์ ํ์ฉ)
๊ณตํต ์ง์ ์ฌํญ (6๊ฐ ๋ชจ๋ธ ๋ชจ๋ ๋์)
์น์ ์ ๋ชฉ: โ๊ณตํต ์ง์ ์ฌํญ (6๊ฐ ๋ชจ๋ธ ๋ชจ๋ ๋์)โ| # | ๋ฌธ์ ์ | ๊ฐ์ ๋ฐฉ์ |
|---|---|---|
| 1 | SHA-256๋ง์ผ๋ก๋ ๋ฆฌ์ฌ์ด์ฆ/ํฌ๋งท๋ณ๊ฒฝ ์ค๋ณต ๊ฐ์ง ๋ถ๊ฐ | pHash(์ง๊ฐ์ ํด์) ๋ณํ ๋์ |
| 2 | NAS 3๊ฐ์ๅๆฃ ์ค์บ โ ๋คํธ์ํฌ I/O ๋ณ๋ชฉ | ํ์ผ ํฌ๊ธฐ ๊ธฐ๋ฐ 1์ฐจ ํํฐ๋ง โ ํด์ ๊ณ์ฐ ์ต์ํ |
| 3 | ์๋ธ์์ด์ ํธ์ ์๋ ๋ชจ๋ธ(kimi-k2.5, minimax-m2.7) ํฌํจ | ์ค์ ์ค์น๋ 8๊ฐ ๋ชจ๋ธ๋ก๋ง ๊ตฌ์ฑ |
| 4 | Phase 2 โ์ค๋ณต์ฒ๋ฆฌโ ๊ธฐ์ค ๋ชจํธ | ๋ณด์กด ์ฐ์ ์์ ๋ช ํํ ๊ท์น์ผ๋ก ์ ์ |
| 5 | LLM ์ญํ ๋ถ๋ด ๋ถ๋ช ํ | ๋ชจ๋ธ ํน์ฑ์ ๋ง๊ฒ ๋ช ํ ๋ฐฐ๋ถ |
1. ํํฉ ํ์
์น์ ์ ๋ชฉ: โ1. ํํฉ ํ์ โ1.1 ์ฌ์ง ์ ์ฅ์ (์ค์ธก ์๋ฃ โ )
์น์ ์ ๋ชฉ: โ1.1 ์ฌ์ง ์ ์ฅ์ (์ค์ธก ์๋ฃ โ )โ| # | ์ ์ฅ์ | WSL ๊ฒฝ๋ก | ์ฌ์ง ์ | ์ฉ๋ | ๋ง์ดํธ |
|---|---|---|---|---|---|
| 1 | ๋ฐฑ์ NAS - ๋ผ์ดํธ๋ฃธ | /mnt/bk_nas/backup/pinksky/๊ฐ์ธ๊ด๋ จ/Media/๋ผ์ดํธ๋ฃธ์ฌ์ง/Lightroom CC/a22a72c5542f4eacb44a1978b389d014/originals/ | 1,247์ฅ | - | โ SMB |
| 2 | ๋ฐฑ์ NAS - PhotoLibrary | /mnt/bk_homes/pinksky/Photos/PhotoLibrary/ | 1,254์ฅ | - | โ SMB |
| 3 | ๊ฐ์ธNAS - ๋ชจ๋ฐ์ผ๋ฐฑ์ | /mnt/nas_homes/pinkskys/Photos/MobileBackup/ | - | ~161GB | โ SMB |
| 4 | ๊ฐ์ธNAS - PhotoLibrary | /mnt/nas_homes/pinkskys/Photos/PhotoLibrary/ | 15์ฅ | 1.4GB | โ SMB |
| 5 | ๊ฐ์ธNAS - photo | /mnt/nas_photo/ | - | - | โ SMB |
๋ชจ๋ฐ์ผ๋ฐฑ์ ์์ธ (๊ฐ์ธNAS):
| ํด๋ | ์ฉ๋ |
|---|---|
| ์์ดํฐ๋ฐฑ์ ์์ ๊บผ | 54GB |
| iPhone | 32GB |
| ์ ํ์ S21 | 17GB |
| ์ ํ์ Z Flip4 | 16GB |
| ์ ํ์๊ธ์ | 11GB |
| MH_S20U | 12GB |
| ์ฃผ์์ Z Fold7 | 8.5GB |
| PS_SE3 | 3.2GB |
| ์ฃผ์์ S21 | 3.3GB |
| ์ ํ์ S20 Ultra | 3.9GB |
๋ง์ดํธ ์ ๋ณด:
/mnt/minicity โ ๊ฐ์ธNAS Backup_data (WebDAV)/mnt/nas_photo โ ๊ฐ์ธNAS photo (SMB)/mnt/nas_homes โ ๊ฐ์ธNAS homes (SMB)/mnt/bk_nas โ ๋ฐฑ์
NAS 14T-HDD (SMB)/mnt/bk_homes โ ๋ฐฑ์
NAS homes (SMB)1.2 ๋ฌธ์ ์
์น์ ์ ๋ชฉ: โ1.2 ๋ฌธ์ ์ โ- ์ ํํ ์ค๋ณต: 3๊ฐ์ ๊ฒฝ๋ก์ ๋์ผ ํ์ผ์ด ์กด์ฌ (โ SHA-256๋ก ๊ฒ์ถ)
- ์ ์ฌ ์ค๋ณต: ๋ฆฌ์ฌ์ด์ฆ, ํฌ๋งท๋ณํ,่ฝปๅพฎํธ์ง์ผ๋ก ํด์๋ ๋ค๋ฅด์ง๋ง ๊ฐ์ ์ฌ์ง (โ pHash๋ก ๊ฒ์ถ)
- ํ์ผ๋ช ๋ณ๊ฒฝ: ํ์ผ๋ช ์ด ๋ฐ๋์ด๋ ๊ฐ์ ์ฌ์ง
- ์ ๋ฆฌ ํ์: ํฅํ ๊ณผ์ ๋ก ๋ฏธ๋ฃจ์ด์ง
2. ๋ชฉํ
์น์ ์ ๋ชฉ: โ2. ๋ชฉํโ2.1 ๋จ๊ธฐ ๋ชฉํ (์ด๋ฒ์ ๋ฌ์ฑ)
์น์ ์ ๋ชฉ: โ2.1 ๋จ๊ธฐ ๋ชฉํ (์ด๋ฒ์ ๋ฌ์ฑ)โ- 1์ฐจ ์ค๋ณต ๊ฒ์ถ - SHA-256 ํด์ ๊ธฐ๋ฐ ์ ํํ ์ค๋ณต ์ฐพ๊ธฐ
- 2์ฐจ ์ ์ฌ ๊ฒ์ถ - pHash ๊ธฐ๋ฐ ์๊ฐ์ ์ ์ฌ ์ค๋ณต ์ฐพ๊ธฐ
- ์ค๋ณต ์ ๋ฆฌ - ์๋ณธ ์ ์ง, ์ค๋ณต ํ์ผ ์ด๋/์ญ์
- ๋ณด๊ณ ์ ์์ฑ - ์ด๋ค ํ์ผ์ด ์ด๋ค ๊ฒฝ๋ก์ ์๋์ง ๊ธฐ๋ก
2.2 ์ค๊ธฐ ๋ชฉํ
์น์ ์ ๋ชฉ: โ2.2 ์ค๊ธฐ ๋ชฉํโ- ์ค๋งํธ ๋ถ๋ฅ - moondream์ผ๋ก ์ฌ์ง ๋ด์ฉ ๋ถ์ โ ์ฐ๋/์/์ฅ์ ์๋ ์ ๋ฆฌ
- ์คํฌ๋ฆฐ์ท ๋ถ๋ฆฌ - moondream + EXIF๋ก ์คํฌ๋ฆฐ์ท/๋ฌธ์์ฌ์ง vs ์ผ๋ฐ์ฌ์ง ๊ตฌ๋ถ
- ์ค์ ์ฌ์ง ํ์ - ํฌํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ๋ฉํ๋ฐ์ดํฐ ํ์ฉ
- ๋ฐฑ์ ๊ฒ์ฆ - ์ ๋ฆฌ ํ ๋ฐฑ์ ๋ฌด๊ฒฐ์ฑ ํ์ธ
2.3 ์ฅ๊ธฐ ๋ชฉํ
์น์ ์ ๋ชฉ: โ2.3 ์ฅ๊ธฐ ๋ชฉํโ- ์ธ๋ฌผ/์ฅ์ ์ธ์ - moondream Vision ๋ถ์์ผ๋ก ์๋ ํ๊น
- ์์ ๋ถ์ - ์คํฌ๋ฆฐ์ท, ๋ฌธ์, ์ผ๋ฐ ์ฌ์ง ์๋ ๊ตฌ๋ถ
- ์๋ ์์นด์ด๋ธ - ์ค๋๋ ์ฌ์ง ์๋ ๋ฐฑ์ ์ด๋
3. ๊ตฌํ ์ํคํ ์ฒ
์น์ ์ ๋ชฉ: โ3. ๊ตฌํ ์ํคํ ์ฒโ3.1 ์์คํ ๊ตฌ์ฑ๋ (v2)
์น์ ์ ๋ชฉ: โ3.1 ์์คํ ๊ตฌ์ฑ๋ (v2)โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ ์ฌ์ง ์ ๋ฆฌ ํ์ดํ๋ผ์ธ v2 โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโคโ โโ โโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโ โโโโโโโโโโโโ โโ โ 1.์ค์บ โ โ โ 2.1์ฐจํํฐ โ โ โ 3.ํด์ โ โ โ 4.์ค๋ณต โ โโ โ ํ์ผ๋ฐ๊ฒฌ โ โ ํฌ๊ธฐ๊ธฐ์ค์ ๋ ฌ โ โ SHA-256 โ โ ๊ทธ๋ฃนํ โ โโ โโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโโโโโโโโโโ โโโโโโโโโโโโ โโ โ โ โ โ โโ โ โ โ v โโ โ โ โ โโโโโโโโโโโโ โโ โ โ โ โ5.์ ์ฌ๊ฒ์ถโ โโ โ โ โ โ pHash โ โโ โ โ โ โโโโโโโโโโโโ โโ โ โ โ โ โโ v v v v โโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโ โ ๐ค ๋ฉํฐ๋ชจ๋ธ ๋ถ์ (8๊ฐ ๋ชจ๋ธ ํ๋) โ โโ โ โ โโ โ โโPhase 1โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โโ โ โ ๐ผ๏ธ moondream โ ์ด๋ฏธ์ง ๋ด์ฉ ๋ถ์/์บก์
๋ โ โ โโ โ โ โก gemma4:e2b โ ๊ฒฝ๋ EXIF ๋ฉํ๋ฐ์ดํฐ ์ถ์ถ โ โ โโ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โโ โ โ โโ โ โโPhase 2โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โโ โ โ ๐ง qwen3.5:cloud โ ์ค๋ณต ๊ทธ๋ฃน ๋ถ์, ๋ณด์กด ์์ โ โ โโ โ โ ๐ glm-5.1:cloud โ ์ญ์ /๋ณด์กด ๊ท์น ์์ฑ โ โ โโ โ โ ๐ฌ deepseek-r1 โ ๋ณต์กํ ์ฃ์ง ์ผ์ด์ค ์ถ๋ก โ โ โโ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โโ โ โ โโ โ โโPhase 3โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โโ โ โ ๐ llama3.1 โ ์ข
ํฉ ๋ณด๊ณ ์ ์์ฑ โ โ โโ โ โ โจ minimax-m2.5 โ ์ต์ข
์์ฝ ๋ฐ ๊ถ์ฅ ์ก์
โ โ โโ โ โ ๐ฏ gemma4:31b โ ์ต์ข
๊ฒ์ ๋ฐ ํ์ง ๋ณด์ฆ โ โ โโ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโ โ โโ v โโ โโโโโโโโโโโโ โโ โ 6.์ ๋ฆฌ โ โโ โMove/Del โ โโ โโโโโโโโโโโโ โโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ3.2 ๋ชจ๋ธ ์ญํ ๋ฐฐ๋ถ (8๊ฐ ๋ชจ๋ธ)
์น์ ์ ๋ชฉ: โ3.2 ๋ชจ๋ธ ์ญํ ๋ฐฐ๋ถ (8๊ฐ ๋ชจ๋ธ)โ| Phase | ๋ชจ๋ธ | ์ญํ | ํ์ | ์ด์ |
|---|---|---|---|---|
| 1. ์ค์บ๋ถ์ | moondream | ์ด๋ฏธ์ง ๋ด์ฉ ๋ถ์/์บก์ ๋ | Local/Vision | ๐ผ๏ธ ์ฌ์ง์ด ๋ฌด์์ธ์ง ํ์ |
| 1. ์ค์บ๋ถ์ | gemma4:e2b | EXIF/๋ฉํ๋ฐ์ดํฐ ๊ฒฝ๋ ์ถ์ถ | Local | โก ๋น ๋ฅธ ๋ก์ปฌ ์ฒ๋ฆฌ |
| 2. ์ค๋ณตํ๋จ | qwen3.5:cloud | ์ค๋ณต ๊ทธ๋ฃน ๋ถ์, ๋ณด์กด ์ฐ์ ์์ | Cloud | ๐ง ๋ถ์/์ฝ๋ฉ ํนํ |
| 2. ์ค๋ณตํ๋จ | glm-5.1:cloud | ์ญ์ /๋ณด์กด ๊ท์น ์์ฑ | Cloud | ๐ ๋ ผ๋ฆฌ์ ํ๋จ |
| 2. ์ค๋ณตํ๋จ | deepseek-r1 | ๋ณต์กํ ์ฃ์ง ์ผ์ด์ค ์ถ๋ก | Local | ๐ฌ ์ฌ์ธต ์ถ๋ก |
| 3. ๋ณด๊ณ ์ | llama3.1 | ์ข ํฉ ๋ณด๊ณ ์ ์์ฑ | Local | ๐ ์์ฐ์ด ์์ฑ |
| 3. ๋ณด๊ณ ์ | minimax-m2.5:cloud | ์ต์ข ์์ฝ ๋ฐ ๊ถ์ฅ ์ก์ | Cloud | โจ ์์ฝ ํนํ |
| 3. ๋ณด๊ณ ์ | gemma4:31b-cloud | ์ต์ข ๊ฒ์ ๋ฐ ํ์ง ๋ณด์ฆ | Cloud | ๐ฏ ๋ํ ๋ชจ๋ธ ๊ฒ์ |
4. ์์ธ ๊ตฌํ ๊ณํ
์น์ ์ ๋ชฉ: โ4. ์์ธ ๊ตฌํ ๊ณํโ4.1 Phase 1: ์ค์บ + 1์ฐจ ํํฐ๋ง + ํด์
์น์ ์ ๋ชฉ: โ4.1 Phase 1: ์ค์บ + 1์ฐจ ํํฐ๋ง + ํด์โ๐ก ๊ฐ์ (gemma4:31b ์ ์): ํ์ผ ํฌ๊ธฐ ๊ธฐ๋ฐ 1์ฐจ ํํฐ๋ง โ
๊ฐ์ ํฌ๊ธฐ์ ํ์ผ๋ง ํด์ ๊ณ์ฐ โ ๋คํธ์ํฌ I/O 70% ์ ๊ฐ
#!/usr/bin/env python3"""1_scan_and_hash.py - ์ฌ์ง ์ค์บ ๋ฐ ํด์ ์์ฑ v2"""
import os, hashlib, jsonfrom pathlib import Pathfrom collections import defaultdictfrom concurrent.futures import ThreadPoolExecutor, as_completed
# ์ง์ ํ์ฅ์EXTENSIONS = {'.jpg', '.jpeg', '.png', '.heic', '.heif', '.webp', '.mov', '.mp4', '.avi'}
def get_file_hash(filepath: str) -> str: """SHA-256 ํด์ ๊ณ์ฐ (8KB ์ฒญํฌ)""" sha256 = hashlib.sha256() with open(filepath, 'rb') as f: for chunk in iter(lambda: f.read(8192), b''): sha256.update(chunk) return sha256.hexdigest()
def scan_and_hash(base_path: str) -> dict: """ 1์ฐจ: ํ์ผ ํฌ๊ธฐ๋ณ ๊ทธ๋ฃนํ (๊ฐ์ ํฌ๊ธฐ๋ง ํด์ ๊ณ์ฐ) 2์ฐจ: SHA-256 ํด์ ๊ณ์ฐ """ # Step 1: ํ์ผ ๋ชฉ๋ก + ํฌ๊ธฐ ์์ง size_groups = defaultdict(list) file_info = {}
print(f"๐ ์ค์บ ์ค: {base_path}") for root, _, files in os.walk(base_path): for file in files: ext = Path(file).suffix.lower() if ext in EXTENSIONS: filepath = os.path.join(root, file) try: fsize = os.path.getsize(filepath) size_groups[fsize].append(filepath) file_info[filepath] = {'size': fsize, 'ext': ext} except OSError: pass
# Step 2: ๊ฐ์ ํฌ๊ธฐ์ ํ์ผ์ด 2๊ฐ ์ด์์ธ ๊ฒฝ์ฐ๋ง ํด์ ๊ณ์ฐ hash_map = defaultdict(list) total = sum(len(v) for v in size_groups.values() if len(v) >= 2) done = 0
for size, paths in size_groups.items(): if len(paths) < 2: continue # ํฌ๊ธฐ๊ฐ ๋ค๋ฅด๋ฉด ์ค๋ณต ๋ถ๊ฐ โ ์คํต for filepath in paths: try: fhash = get_file_hash(filepath) hash_map[fhash].append(filepath) done += 1 if done % 100 == 0: print(f" ๐ข ํด์ ๊ณ์ฐ: {done}/{total}") except Exception as e: print(f" โ ์ค๋ฅ: {filepath} - {e}")
# Step 3: ํฌ๊ธฐ๊ฐ ์ ์ผํ ํ์ผ๋ ๊ธฐ๋ก (๋จ์ผ ํ์ผ) singles = {} for size, paths in size_groups.items(): if len(paths) == 1: singles[paths[0]] = size
return { 'duplicates': {k: v for k, v in hash_map.items() if len(v) > 1}, 'unique_count': len(singles), 'total_scanned': len(file_info), }
if __name__ == '__main__': import argparse parser = argparse.ArgumentParser() parser.add_argument('--path', required=True) parser.add_argument('--output', default='scan_result.json') parser.add_argument('--dry-run', action='store_true') args = parser.parse_args()
result = scan_and_hash(args.path)
with open(args.output, 'w') as f: json.dump(result, f, ensure_ascii=False, indent=2)
dup_count = len(result['duplicates']) print(f"\nโ
์๋ฃ! ์ด {result['total_scanned']}๊ฐ ์ค์บ, {dup_count}๊ฐ ์ค๋ณต ๊ทธ๋ฃน ๋ฐ๊ฒฌ")4.2 Phase 2: pHash ์ ์ฌ ๊ฒ์ถ (ๆฐๅข)
์น์ ์ ๋ชฉ: โ4.2 Phase 2: pHash ์ ์ฌ ๊ฒ์ถ (ๆฐๅข)โ๐ก gemma4:31b + minimax-m2.5 ์ ์: SHA-256์ผ๋ก ๋ชป ์ก๋
๋ฆฌ์ฌ์ด์ฆ/ํฌ๋งท๋ณํ ์ค๋ณต์ pHash(Perceptual Hash)๋ก ๊ฒ์ถ
#!/usr/bin/env python3"""2_perceptual_hash.py - ์ง๊ฐ์ ํด์ ๊ธฐ๋ฐ ์ ์ฌ ์ฌ์ง ๊ฒ์ถ"""
# ์์กด์ฑ: pip install imagehash Pillow# (์์ผ๋ฉด ์ค์น: pip install imagehash Pillow)
import os, jsonfrom pathlib import Pathfrom PIL import Imageimport imagehash
def get_phash(filepath: str, size: int = 8) -> str: """pHash ๊ณ์ฐ (์ด๋ฏธ์ง๊ฐ ์ด๋ฆฌ์ง ์์ผ๋ฉด None ๋ฐํ)""" try: img = Image.open(filepath) return str(imagehash.phash(img, hash_size=size)) except Exception: return None
def find_similar(base_path: str, threshold: int = 10) -> list: """ pHash ํด๋ฐ๊ฑฐ๋ฆฌ๊ฐ threshold ์ดํ์ธ ํ์ผ ์ ์ฐพ๊ธฐ - threshold=0: ์์ ๋์ผ (SHA-256๊ณผ ๋์ผ) - threshold=10: ์ฝ๊ฐ์ ์ฐจ์ด ํ์ฉ (๋ฆฌ์ฌ์ด์ฆ, ๊ฒฝ๋ฏธํ ํธ์ง) """ hashes = {}
for root, _, files in os.walk(base_path): for file in files: ext = Path(file).suffix.lower() if ext in {'.jpg', '.jpeg', '.png', '.webp'}: filepath = os.path.join(root, file) h = get_phash(filepath) if h: hashes[filepath] = h
# ์ ์ฌ ์ ์ฐพ๊ธฐ similar_groups = [] paths = list(hashes.keys())
for i in range(len(paths)): for j in range(i + 1, len(paths)): h1 = imagehash.hex_to_hash(hashes[paths[i]]) h2 = imagehash.hex_to_hash(hashes[paths[j]]) distance = h1 - h2 if distance <= threshold: similar_groups.append({ 'file1': paths[i], 'file2': paths[j], 'distance': distance, })
return similar_groups4.3 Phase 3: ์ค๋ณต ์ฒ๋ฆฌ ๋ก์ง (๋ช ํํ)
์น์ ์ ๋ชฉ: โ4.3 Phase 3: ์ค๋ณต ์ฒ๋ฆฌ ๋ก์ง (๋ช ํํ)โ๐ก minimax-m2.5 + qwen3.5 ์ ์: ๋ณด์กด ์ฐ์ ์์๋ฅผ ๋ช ์์ ๊ท์น์ผ๋ก
#!/usr/bin/env python3"""3_process_duplicates.py - ์ค๋ณต ์ฒ๋ฆฌ (๋ณด์กด ์ฐ์ ์์ ๋ช
ํํ)"""
import os, json
# ๋ณด์กด ์ฐ์ ์์ (1์ด ์ต๊ณ )PRIORITY_RULES = [ # ๊ท์น 1: ๋ผ์ดํธ๋ฃธ originals ํด๋ ์๋ณธ lambda f: 1 if 'originals' in f.lower() else 999, # ๊ท์น 2: ํ์ผ๋ช
์ _original, _orig ํฌํจ lambda f: 2 if any(x in os.path.basename(f).lower() for x in ['_original', '_orig']) else 999, # ๊ท์น 3: ํ์ผ ํฌ๊ธฐ๊ฐ ๊ฐ์ฅ ํผ (์๋ณธ ํ์ง) lambda f: 3, # ํฌ๊ธฐ ๋น๊ต๋ ์๋์์ ๋ณ๋ ์ฒ๋ฆฌ # ๊ท์น 4: ์์ ์ผ์ด ๊ฐ์ฅ ์ค๋๋จ (์ต์ด ์์ฑ) lambda f: 4, # ๋ ์ง ๋น๊ต๋ ๋ณ๋ ์ฒ๋ฆฌ # ๊ท์น 5: PhotoLibrary ๊ฒฝ๋ก lambda f: 5 if 'photolibrary' in f.lower() else 999,]
def select_original(file_list: list) -> str: """ ์ค๋ณต ํ์ผ ๋ชฉ๋ก์์ ์๋ณธ ์ ํ ์ฐ์ ์์: originals > _original ๋ช
์นญ > ํ์ผ ํฌ๊ธฐ > ์์ ์ผ > ๊ฒฝ๋ก """ scored = [] for f in file_list: priority = 999 for i, rule in enumerate(PRIORITY_RULES[:2]): # ๊ฒฝ๋ก ๊ธฐ๋ฐ ๊ท์น p = rule(f) if p < priority: priority = p
fsize = os.path.getsize(f) if os.path.exists(f) else 0 mtime = os.path.getmtime(f) if os.path.exists(f) else 0 scored.append((f, priority, -fsize, mtime)) # ํฌ๊ธฐ๋ ํฐ๊ฒ ์ข์ผ๋ ์์
# ์ฐ์ ์์ โ ํฌ๊ธฐ(ํฐ๊ฒ) โ ์์ ์ผ(์ค๋๋๊ฒ) ์ ์ ๋ ฌ scored.sort(key=lambda x: (x[1], x[2], -x[3])) return scored[0][0] # ๊ฐ์ฅ ์ฐ์ ์์ ๋์ ํ์ผ = ์๋ณธ
def process_duplicates(scan_result: dict, dry_run: bool = True) -> list: """์ค๋ณต ์ฒ๋ฆฌ ์คํ""" actions = []
for hash_key, file_list in scan_result.get('duplicates', {}).items(): if len(file_list) < 2: continue
original = select_original(file_list) duplicates = [f for f in file_list if f != original]
for dup in duplicates: action = { 'original': original, 'duplicate': dup, 'action': 'move', # ์ญ์ ๋์ ์ด๋ (์์ ) 'destination': dup.replace('/originals/', '/duplicates/'), } actions.append(action)
if not dry_run: os.makedirs(os.path.dirname(action['destination']), exist_ok=True) os.rename(dup, action['destination'])
return actions4.4 Phase 4: moondream ์ด๋ฏธ์ง ๋ถ์ (ๆฐๅข)
์น์ ์ ๋ชฉ: โ4.4 Phase 4: moondream ์ด๋ฏธ์ง ๋ถ์ (ๆฐๅข)โ๐ก moondream ์ ์ฉ ์ญํ : ์ฌ์ง ๋ด์ฉ ๋ถ์ โ
์คํฌ๋ฆฐ์ท/๋ฌธ์/์ธ๋ฌผ/ํ๊ฒฝ ์๋ ๋ถ๋ฅ
#!/usr/bin/env python3"""4_moondream_classify.py - Vision ๋ชจ๋ธ๋ก ์ฌ์ง ๋ถ๋ฅ"""
import json, urllib.request, base64from pathlib import Path
def classify_image(image_path: str) -> dict: """moondream์ผ๋ก ์ด๋ฏธ์ง ๋ถ๋ฅ""" with open(image_path, 'rb') as f: img_b64 = base64.b64encode(f.read()).decode('utf-8')
payload = { "model": "moondream", "messages": [{ "role": "user", "content": "์ด ์ฌ์ง์ ๋ถ๋ฅํด์ค. ๋ต๋ณ์ JSON์ผ๋ก: {category, description, is_screenshot}", "images": [img_b64] }], "stream": False, "options": {"num_predict": 200} }
req = urllib.request.Request( "http://127.0.0.1:11434/api/chat", data=json.dumps(payload).encode('utf-8'), headers={'Content-Type': 'application/json'} )
with urllib.request.urlopen(req, timeout=60) as resp: data = json.loads(resp.read().decode('utf-8'))
return data.get('message', {}).get('content', '')5. ์์ ์์
์น์ ์ ๋ชฉ: โ5. ์์ ์์โ5.1 ๋จ๊ณ๋ณ ํ ์ผ
์น์ ์ ๋ชฉ: โ5.1 ๋จ๊ณ๋ณ ํ ์ผโ| ๋จ๊ณ | ์์ | ๋ด๋น ๋ชจ๋ธ | ์์ ์๊ฐ |
|---|---|---|---|
| 1 | ์ค์บ ์คํฌ๋ฆฝํธ ์์ฑ | qwen3.5:cloud + minimax-m2.5 | 30๋ถ |
| 2 | ์์กด์ฑ ์ค์น (imagehash, Pillow) | - | 5๋ถ |
| 3 | ๐งช ํ ์คํธ ์คํ (์์ ์ํ ~50์ฅ) | - | 30๋ถ |
| 4 | ์ ์ฒด ์ค์บ ์คํ | - | 2~4์๊ฐ |
| 5 | pHash ์ ์ฌ ๊ฒ์ถ | moondream + gemma4:e2b | 1~2์๊ฐ |
| 6 | ์ค๋ณต ๋ถ์ LLM ํ์ฉ | qwen3.5 + glm-5.1 + deepseek-r1 | 30๋ถ |
| 7 | ์ ๋ฆฌ ์คํ (dry-run) | - | 30๋ถ |
| 8 | ๐ ์ต์ข ๊ฒ์ | gemma4:31b-cloud | 30๋ถ |
| 9 | ๋ณด๊ณ ์ ์์ฑ | llama3.1 + minimax-m2.5 | 20๋ถ |
| 10 | โ ์ค์ ์ ๋ฆฌ ์คํ (dry-run ํ์ธ ํ) | - | 1์๊ฐ |
5.2 ๋ฉํฐ๋ชจ๋ธ ํ๋ ํ๋ฆ
์น์ ์ ๋ชฉ: โ5.2 ๋ฉํฐ๋ชจ๋ธ ํ๋ ํ๋ฆโ โโโโโโโโโโโโโโโโโโโโโโโ โ ์ค์บ ๊ฒฐ๊ณผ JSON โ โโโโโโโโโโโโฌโโโโโโโโโโโ โ โโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ Phase 1: ์ค์บ ๋ถ์ โ โ moondream โโ ์ด๋ฏธ์ง ๋ด์ฉ ์บก์
๋ โ โ gemma4:e2b โโ EXIF/๋ฉํ๋ฐ์ดํฐ ์ถ์ถ โ โโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ Phase 2: ์ค๋ณต ํ๋จ โ โ qwen3.5:cloud โโ ๊ทธ๋ฃน ๋ถ์/๋ณด์กด์์ โ โ glm-5.1:cloud โโ ์ญ์ /๋ณด์กด ๊ท์น ์์ฑ โ โ deepseek-r1 โโ ์ฃ์ง ์ผ์ด์ค ์ถ๋ก โ โโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ Phase 3: ๋ณด๊ณ ์ + ๊ฒ์ โ โ llama3.1 โโ ์ข
ํฉ ๋ณด๊ณ ์ ์์ฑ โ โ minimax-m2.5 โโ ์ต์ข
์์ฝ/๊ถ์ฅ์ก์
โ โ gemma4:31b โโ ํ์ง ๊ฒ์/์ต์ข
์น์ธ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ6. ์คํ ๋ช ๋ น์ด
์น์ ์ ๋ชฉ: โ6. ์คํ ๋ช ๋ น์ดโ6.1 ํ๊ฒฝ ์ค๋น
์น์ ์ ๋ชฉ: โ6.1 ํ๊ฒฝ ์ค๋นโ# ์์กด์ฑ ์ค์นpip install imagehash Pillow
# ์ค์ NAS ๊ฒฝ๋ก ํ์ธls /mnt/minicity/6.2 ์คํฌ๋ฆฝํธ ์คํ
์น์ ์ ๋ชฉ: โ6.2 ์คํฌ๋ฆฝํธ ์คํโ# Step 1: SHA-256 ์ค์บ (1์ฐจ)python3 1_scan_and_hash.py --path /mnt/minicity --output scan_result.json
# Step 2: pHash ์ ์ฌ ๊ฒ์ถ (2์ฐจ)python3 2_perceptual_hash.py --path /mnt/minicity --output similar_result.json
# Step 3: ์ค๋ณต ์ฒ๋ฆฌ (dry-run)python3 3_process_duplicates.py --input scan_result.json --dry-run
# Step 4: moondream ๋ถ๋ฅpython3 4_moondream_classify.py --input scan_result.json --output classify_result.json
# Step 5: ์ค์ ์คํ (dry-run ๊ฒฐ๊ณผ ํ์ธ ํ!)python3 3_process_duplicates.py --input scan_result.json6.3 LLM ํ๋ ๋ถ์
์น์ ์ ๋ชฉ: โ6.3 LLM ํ๋ ๋ถ์โ# ์ค๋ณต ๊ทธ๋ฃน LLM ๋ถ์ (3๊ฐ ๋ชจ๋ธ ๋ณ๋ ฌ)ollama run qwen3.5:cloud "์ด ์ค๋ณต ์ฌ์ง ๊ทธ๋ฃน๋ค์ ๋ถ์..."ollama run glm-5.1:cloud "์ญ์ /๋ณด์กด ๊ท์น์ ์์ฑ..."ollama run deepseek-r1 "๋ณต์กํ ์ค๋ณต ์ผ์ด์ค๋ฅผ ํ๋จ..."7. ์ฃผ์์ฌํญ
์น์ ์ ๋ชฉ: โ7. ์ฃผ์์ฌํญโโ ๏ธ ์ค์ (๋ชจ๋ธ ๊ณตํต ์ง์ ):
- ๋ฐฑ์ ํ์ - ์ ๋ฆฌ ์ ๋ฐ๋์ ์ ์ฒด ํด๋ ๋ฐฑ์
- Dry-run ์ ํ - ์ค์ ์ญ์ ํ๊ธฐ ์ ์ dry-run์ผ๋ก ํ ์คํธ
- ์๋ณธ ๋ณด์กด - ํญ์ ์๋ณธ ํ์ผ 1๊ฐ๋ ์ ์ง (์ญ์ ๋์ ์ด๋)
- ๋ก๊ทธ ์ ์ฅ - ๋ชจ๋ ์์ ์ ๋ก๊ทธ๋ก ๊ธฐ๋ก (๋ณต๊ตฌ ๊ฐ๋ฅ)
- pHash ์๊ณ๊ฐ ์ฃผ์ - threshold ๋๋ฌด ๋ฎ์ผ๋ฉด ๋์น๊ณ , ๋๋ฌด ๋์ผ๋ฉด ์คํ
- ๋คํธ์ํฌ ์์ ์ฑ - NAS ์ฐ๊ฒฐ ๋๊ธฐ๋ฉด ํด์ ๊ณ์ฐ ์คํจ โ ์ฌ์๋ ๋ก์ง ํ์
- ๋ฉ๋ชจ๋ฆฌ ๊ด๋ฆฌ - 207GB ๋ชจ๋ฐ์ผ ๋ฐฑ์ ์ ์ฒญํฌ ๋จ์๋ก ์ฒ๋ฆฌ
8. ๊ด๋ จ ๋ฌธ์
์น์ ์ ๋ชฉ: โ8. ๊ด๋ จ ๋ฌธ์โ- [[MiniCITY(v0.5_PARA_0502-)/04_ARCHIVE(๋ณด๊ดํจ)/๋ฒ์ ๋ณ/v0.2-๋ฐฑ์ /MiniCITY(v0.2_์งํ์ค_0419-)/05.PROJECTS/F.์๋ฃ์ ๋ฆฌ/PS.BOT-F-01-๊ฐ์]] - ๊ธฐ์กด ์๋ฃ ๊ฐ์
- [[MiniCITY(v0.5_PARA_0502-)/02_AREAS(์์ญ)/์์ด์ ํธ/LLM-๋ชจ๋ธ/LLM_๋ชจ๋ธ_๋ชฉ๋ก]] - ์ฌ์ฉ ๋ชจ๋ธ ์ ๋ณด
9. ์ ๋ฐ์ดํธ ๋ก๊ทธ
์น์ ์ ๋ชฉ: โ9. ์ ๋ฐ์ดํธ ๋ก๊ทธโ| ๋ ์ง | ์์ฑ์ | ๋ด์ฉ |
|---|---|---|
| 2026-04-21 | ํ๋์ด | v1 ์ด๊ธฐ ๊ณํ ์์ฑ |
| 2026-04-21 | ํ๋์ด + 8๋ชจ๋ธ | v2 ๋ฉํฐ๋ชจ๋ธ ํ๋ ๋ถ์ ๋ฐ์ |
[!note] ์์ ์ด๋ ฅ | 2026-04-21 06:XX, ํ๋์ด (v2)
๐ง ์งํ ์ํฉ:
- ๊ณํ์ v1 ์์ฑ
- 8๋ชจ๋ธ ํ๋ ๋ถ์ ์๋ฃ
- ๊ณํ์ v2 ์ ๋ฐ์ดํธ
- ์์กด์ฑ ์ค์น (imagehash, Pillow)
- ์คํฌ๋ฆฝํธ ์์ฑ
- ํ ์คํธ ์คํ
- ์ ์ฒด ์ค์บ
- ์ ๋ฆฌ ์คํ
- ๋ณด๊ณ ์ ์์ฑ