Organize your NAS files using a local LLM (Ollama). Scans, classifies, reviews, and moves files with full undo capability.
# Prerequisites
ollama pull qwen2.5-coder:7b # or use any model you prefer
DS=~/.venvs/data-sorter/bin/python # venv lives on LOCAL disk (see Installation)
# Run the sorter (dry-run first!)
$DS sort.py --dry-run # Preview changes without moving anything
# Full run (interactive review → execute)
$DS sort.py
# Undo the last sort
$DS sort.py --undo┌─────────┐ ┌──────────┐ ┌───────────┐ ┌────────┐ ┌─────────┐
│ Scanner │ → │Extractor │ → │Classifier │ → │Reviewer│ → │Executor │
│ (files) │ │(content) │ │ (Ollama) │ │ (TUI) │ │ (moves) │
└─────────┘ └──────────┘ └───────────┘ └────────┘ └─────────┘
↑
┌──────────┐
│ Deduper │
│(duplicates│
└──────────┘
- Scanner — discovers all files on the NAS, respects
.sortignore - Extractor — extracts text (PDFs, Office docs) and metadata (EXIF, video/audio info)
- Classifier — sends file info to Ollama in batches → LLM suggests target category + confidence
- Deduper — detects duplicate files (SHA-256 exact + perceptual hash for images)
- Reviewer — Rich TUI shows colored preview tree; you accept/modify/reject per file
- Executor — moves files, logs everything to CSV for undo
The NAS is a physical place you walk through, not a database you query. Navigation is walking; search is the fallback. Six principles govern every destination:
- MM-1 — Filesystem is a place, not a database. Every destination is reachable by walking (folder browsing, not querying). Search stays a fallback.
- MM-2 — At most 3 levels. Destinations sit ≤ 3 folder levels below the NAS root (
top/category/leaf); wide over deep.Projects/andZeno/Documents/Design/are deep containers that keep their own internal trees. - MM-3 — One clean subject per leaf. Context never adds a nesting level: a leaf is named for its owner, event, or subject — never
A_Bstitched names, never repeated generic words (Bilder,pics,screenshots, …). - MM-4 — Structure matches content.
Projects= tree,Media/doc areas = cluster,Archives= preserve. Dates are metadata, never folder chains. - MM-5 — Consistency over cleverness. The LLM classifies, deterministic code normalizes.
- MM-6 — One deliberate pile.
_Unsorted_Review/holds everything uncertain — folders move there as units, never scattered file-by-file.
Enforcement — the policy is deterministic, not a suggestion:
| Rule | Where it lives |
|---|---|
| depth limit, deep containers, per-branch structures | config.yaml → taxonomy.placement_max_depth: 3, deep_containers: [Projects, Zeno/Documents/Design], structures (Zeno/Family/Company/Media = cluster, Projects = tree, Archives = preserve) |
| depth fit, subject-leaf fold, container bypass | sorter/placement.py → fit_to_depth(), is_under_container() (overflow → fold cat + leaf, note [depth-fit], review if unfittable) |
| LLM-facing rules | generated {placement_rules} block (placement_rules_text()), injected into the batch prompt (sorter/classifier.py) and folder prompt (sort.py) |
Behavioral notes: raw personal clips → Zeno/Videos, collected/finished media → Media/Videos, video-production work → Projects/Video_Projects; a proposal adds at most ONE new subfolder with category_path ≤ 3 segments total; generic-medium leaves are promoted to their owner/event/subject (a carve-out, never a translation of names).
Examples (before → after):
| Before | After | Why |
|---|---|---|
Diverse Daten/EVE/-Y-/old_G_files/bilder&screens |
Media/Photos/EVE |
generic-medium leaf promoted to owner EVE (junk segments don't disqualify) |
Diverse Daten/Ausbildung/Schule/Projekt Müller |
Zeno/Documents/Projekt Müller |
folded [depth-fit] — ancestor chain dropped, one clean subject leaf kept |
Zeno/Documents/Design/EVE/BO Logo |
unchanged | deep container keeps its internal tree |
Projects/Hypnose/Audio/Hypnose |
unchanged | deep container — projects are trees, deep is fine |
DELETE / uncertain-review actions |
untouched by fit | no path rewrite on those actions |
Rationale: Benn et al. 2015 (Scientific Reports) — browsing digital folders engages the same brain structures as spatial navigation (vs. search = linguistic resources); Kemp & Tenenbaum 2008 (PNAS) — no single best structure; it must match the data (trees for projects, clusters for topics). Machine-readable policy: .docs/mental-model.yaml.
- Mandatory dry run — preview every change before anything moves
- Confidence thresholds — 🟢 green (≥85%) auto-accepted, 🟡 yellow (50-85%) user reviews, 🔴 red (<50%) queued for manual review
- CSV undo log — every move logged with SHA-256 hash;
--undoreverses it - Staging trash — deleted duplicates go to
Archives/_Duplicates_Delete/never hard-deleted - Adobe Premiere temp files — flagged for cleanup (Auto-Save, Preview Files are regeneratable)
# Clone / enter the project directory
cd /mnt/NAS-Zeno/OpenCode/Data\ Sorter
# Use a venv on LOCAL disk — never inside this repo.
# This dir is a CIFS mount that rejects symlinks, so `python3 -m venv .venv`
# fails with Errno 95 on the lib64 -> lib link (even with --copies).
python3 -m venv ~/.venvs/data-sorter
~/.venvs/data-sorter/bin/pip install -r requirements.txt
# Verify (-p no:cacheprovider: this dir is a CIFS mount that rejects pytest's cache writes)
~/.venvs/data-sorter/bin/python -m pytest tests/ -q -p no:cacheprovider
~/.venvs/data-sorter/bin/python sort.py --help- Python 3.11+
- Ollama running locally at
http://localhost:11434 - Model:
qwen2.5-coder:7b(or any model you configure) - NAS mounted at
/mnt/NAS-Zeno/(configurable) - Optional:
ffprobe(for video/audio metadata),markitdownauto-installed
| Command | Description |
|---|---|
$DS sort.py |
Dry run → interactive review → execute |
$DS sort.py --dry-run |
Preview only, no changes |
$DS sort.py --undo |
Revert the last sort run |
$DS sort.py --execute |
Skip interactive review; derived/review moves are never auto-applied |
$DS sort.py --path Subfolder |
Scan only a sub-path |
$DS sort.py --config my.yaml |
Use alternate config |
Edit config.yaml to customize:
ollama:
model: "qwen2.5-coder:7b" # LLM model for classification
batch_size: 8 # files per request (KV cache reuse, lower latency)
temperature: 0 # deterministic classification
seed: 42 # reproducible LLM output
timeout: 60 # seconds
confidence:
auto_accept: 85 # 🟢 green threshold
require_review: 50 # 🟡 yellow threshold
taxonomy:
top_level:
- Zeno
- Family
- Company
- Projects
- Media
- Archives
- _Unsorted_Review
placement_max_depth: 3 # MM-2: destinations sit <= 3 levels below NAS root
deep_containers: # MM-2 exception: these keep their internal trees
- Projects
- Zeno/Documents/Design
structures: # MM-4: branch structure by top level
Zeno: cluster
Family: cluster
Company: cluster
Projects: tree
Media: cluster
Archives: preserve
categories:
Zeno:
- Documents, Downloads, Bilder, Videos, Finances, Career, ...
embeddings:
enabled: false # opt-in semantic pre-classification (nomic-embed-text)
model: "nomic-embed-text"
similarity_threshold: 0.85 # centroid cosine threshold; LLM only runs on unmatched filesYour NAS will be organized into:
/mnt/NAS-Zeno/
├── Zeno/ ← Your personal files
│ ├── Documents/ ← PDFs, Office docs, etc.
│ ├── Downloads/
│ ├── Bilder/
│ ├── Videos/
│ ├── Finances/
│ ├── Career/
│ └── Obsidian_Vault/
├── Family/
│ ├── Mom/
│ ├── Andreas/
│ └── Emily/
├── Company/
│ └── 5 Circles/ ← Your company
├── Projects/
│ ├── ERGO Paphos/
│ ├── FC Squad/
│ └── ... (all existing projects)
├── Media/
│ ├── DJ_Stuff/
│ ├── Music/
│ └── Photos/
├── Archives/
│ ├── Old_Projects/
│ ├── System/
│ └── _Duplicates_Delete/
└── _Unsorted_Review/ ← Low-confidence items
LLM can propose new subfolders under any top-level category if no existing one fits.
~/.venvs/data-sorter/bin/python -m pytest tests/ -v -p no:cacheprovider591 tests, all passing.
| Module | File | Responsibility |
|---|---|---|
| Entry | sort.py |
CLI, pipeline orchestration |
| Scanner | sorter/scanner.py |
File discovery, MIME detection, ignore patterns |
| Extractor | sorter/extractor.py |
Content/metadata extraction per file type |
| Classifier | sorter/classifier.py |
Ollama REST client, batch inference (deterministic temp=0/seed=42) |
| Routing | sorter/routing.py |
Deterministic extension→category pre-classifier with path-context guard + delete-protection for 3D models, design sources, media, documents |
| Taxonomy | sorter/taxonomy.py |
Category tree, Adobe Premiere detection |
| Deduper | sorter/deduper.py |
SHA-256 + perceptual hash + copy-suffix duplicate scanner |
| Reviewer | sorter/reviewer.py |
Rich TUI interactive review |
| Executor | sorter/executor.py |
File moves, CSV logging |
| Undo | sorter/undo.py |
CSV reader, reverse file moves |
| Embedding | sorter/embeddings.py |
nomic-embed-text embeddings, centroid matching, KMeans clustering (opt-in) |
See .docs/2026-09-17-nas-file-sorter-design.yaml for full architecture and rationale.
origin is GitHub (https://github.com/Ridoc/data-sorter.git). The NAS copy is
the nas remote: ssh://bienenhaus/volume1/git/data-sorter.git on Bienenhaus
(192.168.1.101), served by a key-only git user. The post-commit hook pushes
nas and mirrors tracked files to
/mnt/bienenhaus-workspace/backups/OpenCode/data-sorter/; full-history
bundles live at /mnt/NAS-Zeno/backups/git/data-sorter.bundle. Restore with
git clone ssh://bienenhaus/volume1/git/data-sorter.git or the bundle.