|
Classical CV, no ML, any CPU |
OpenAI zero-shot vision-language |
CLIP + CV combined, recommended |
Local vision LLMs |
- Highlights
- Why Wanalizer
- Quick start
- Analysis modes
- How classification works
- Multi-signal combiner
- Per-category configuration
- Duplicate detection
- GUI overview
- Project structure
- Installation
- CLI reference
- Tips and recipes
- Roadmap
- License
- Four interchangeable analysis modes sharing a single pipeline abstraction.
- 13-signal multi-scorer with anti-pollution defences so noisy tag registries cannot hijack the result.
- MD5 duplicate detection with a per-folder cache. The second scan is essentially free; per-group keep/move/delete decisions.
- Per-category configuration via an interactive Q&A wizard or AI-suggested rules sampled from your own images.
- Reorganize tab: visual browser with thumbnails, aspect-ratio filter, rename dialog, and per-folder "expected" specifications.
- 50+ image formats supported (JPEG, PNG, WebP, AVIF, HEIC, PSD, TIFF, RAW, SVG, PDF, ...) plus single-frame extraction for videos and animated images via
ffmpeg. - True parallel hashing and classification via
ProcessPoolExecutorand an optional free-threaded Python 3.14t runtime for genuine no-GIL threading. - GUI and CLI from one package. After
pip install .two console scripts are available:wanalyzerandwanalyzer-gui.
Other wallpaper organizers usually have one or more of these limitations:
- Require a remote API (privacy + latency).
- Require a GPU.
- Ship as a black box with no tuning knobs.
- Sort by filename heuristics only.
Wanalizer takes a different approach:
| Concern | Wanalizer |
|---|---|
| Offline / no network | Low-Level CV runs on any CPU. |
| Semantic understanding | CLIP, Fusion, and Ollama modes. |
| Detects duplicates | MD5 with incremental cache. |
| Per-category tuning | Q&A wizard, AI Suggest, or hand-edited JSON. |
| Visual browser | Reorganize page with thumbnails and filters. |
| Multi-signal classifier | 13 weighted signals, pollution-aware. |
| True parallel CPU work | ProcessPoolExecutor + optional free-threaded Python 3.14t. |
| GUI and CLI from one code | Shared pipeline, identical behaviour. |
git clone https://github.com/leo/Wanalizer.git
cd Wanalizer
./run.sh # launch the GUI (auto-installs PySide6)
./run.sh --cli --help # CLI help
./run.sh --cli --mode fusion --dry # preview classification
./run.sh --cli --mode fusion --full # flatten + classify for realIf you prefer to install via pip:
pip install .
wanalyzer --help
wanalyzer-guiFree-threaded mode (true parallel threads, no GIL):
./run.sh --cli --ft --mode fusion --dryThe first run downloads Python 3.14t (~30 MB) automatically.
| Mode | What it uses | Strength | Dependencies | Speed |
|---|---|---|---|---|
| Low-Level CV | Canny / Sobel / Scharr edges, Otsu silhouettes, Hu moments, LBP, GLCM, Gabor, ORB / FAST, FFT, HOG, colour moments, composition, symmetry, pattern detection | Pure statistics, no ML, any CPU | Pillow, numpy, scipy | Fast |
| CLIP | OpenAI CLIP zero-shot (ViT-B/32 by default) | Semantic understanding ("this is Anime, not Gruvbox") | torch, clip | Slow first image, cached |
| Fusion | Low-Level CV + CLIP in parallel, 13-signal fusion | Best of both worlds; recommended when CLIP is available | torch, clip, Pillow, numpy | Slower than CLIP alone |
| Ollama | Local vision LLM via Ollama (LLaVA, MiniCPM-V, Llama 3.2 Vision, Moondream) | Natural-language descriptions, character recognition, NSFW | requests + running Ollama server | Medium |
| Vision API | Any OpenAI-compatible endpoint with vision (/v1/chat/completions + image_url): OpenAI, OpenRouter, LM Studio, Groq, self-hosted vLLM, DeepSeek (DeepSeek-VL / DeepSeek-VL2 via OpenRouter or self-hosted vLLM; confirm vision support on api.deepseek.com) |
Access cloud vision LLMs without a local GPU; provider-portable | requests + base URL / API key / model | Medium (network bound) |
Fusion is a strict superset of Low-Level: it gracefully degrades to Low-Level when CLIP cannot be loaded.
- Source files are scanned from the current directory (or any folder you pass via
--dest/--set-dest). - If dedupe is enabled, MD5s are computed and exact duplicates are moved to a
Duplicates/folder (cached for next time). - Each remaining file is analysed by the selected mode and turned into a profile dict with weights, style scores, theme scores, content detectors, and (where available) CLIP scores.
- The multi-signal combiner scores every category against the profile and picks the best match, optionally weighted by the category's
expectedspecification. - The file is moved into the winning category, optionally renamed according to the strategy in the Organize tab.
The full pipeline lives in wallpaper_analyzer/organize.py and is the same code path used by both the CLI and the GUI.
Every analyzer returns a profile dict. wallpaper_analyzer/classify.py combines 13 weighted signals into a single confidence score per category:
| Signal | Weight | What it measures |
|---|---|---|
| tags | 0.10 | TF-IDF overlap between the image tags and each category's tags |
| palette | 0.10 | Cosine similarity between colour distribution and category palette |
| style | 0.08 | Continuous bonuses for anime_score, skin_fraction, periodicity_score, ... |
| content | 0.05 | Specific detectors: minecraft_score, pixel_art_score, minimalist_score, ... |
| theme | 0.07 | Named themes (Catppuccin, Dracula, TokyoNight, Nord, Gruvbox, ...) |
| composition | 0.05 | Rule-of-thirds and subject-area heuristics |
| quality | 0.03 | Tenengrad + aesthetic + sharpness for photo / illustration categories |
| pattern | 0.04 | Periodicity and tile detection |
| size | 0.03 | Aspect-ratio bucket (phone, vertical, wide, landscape) |
| prompt | 0.08 | TF-IDF cosine between the generated prompt and the category prompt |
| clip | 0.25 | CLIP softmax probabilities (only when CLIP has run) |
| clip_nsfw | 0.05 | CLIP-driven NSFW boost for nsfw / 18+ / ecchi categories |
| fingerprint | 0.20 | Z-score similarity against each category's learned CV fingerprint |
The fingerprint signal is the most reliable because it compares the image's CV profile against the actual feature distribution of each category. The clip signal is the most discriminative when CLIP has run. Tag-based signals have explicit anti-pollution defences so a noisy registry cannot hijack the result.
Each category folder contains a .category.json describing what belongs there. Example for a Cyberpunk folder:
{
"name": "Cyberpunk",
"tags": ["cyberpunk", "neon", "dark", "city"],
"prompt": "Dark futuristic city with neon purple and teal lights",
"palette_weights": {
"Purple": 1.0,
"Pink-Magenta": 0.5,
"Black": 0.3
},
"expected": {
"aspect_ratios": ["horizontal", "square"],
"file_kinds": ["image"],
"min_resolution": [1920, 1080],
"color_palette": ["dark", "cool"],
"style_keywords": ["neon", "illustration"],
"exclude_keywords": ["anime", "photograph"],
"source": "user"
}
}Three ways to create one:
- Interactive Q&A wizard - Categories page, Configure (Q&A).
- AI Suggest - Categories page, AI Suggest. Samples images, runs CLIP + CV, proposes an
expectedblock you can review and save. - Manual JSON edit.
After configuration, the Reorganize tab shows the expected spec in the sidebar tooltip, and classify.config_match_score uses the spec as a soft tie-breaker when classifying.
The Duplicates tab detects exact duplicates via MD5 hashing. The hash cache lives in .wallpaper_analyzer_hashes.json inside the destination folder, so the second and subsequent scans only hash new files.
For each duplicate group:
- The largest file is kept (heuristic for highest quality).
- The rest can be moved to
Duplicates/or deleted permanently. - Per-group decisions or one-click "Move ALL" / "Delete ALL".
CLI shortcut:
./run.sh --cli --find-duplicatesFor visually-similar but byte-different images, the underlying perceptual-hash primitives are still available in wallpaper_analyzer.hashing for advanced callers.
The GUI ships with 9 pages wired into a single QStackedWidget:
| # | Page | What it does |
|---|---|---|
| 0 | Dashboard | Library overview, mode picker, AI health, recent activity log |
| 1 | Organize | Run the classification pipeline; choose source/destination + workers |
| 2 | Reorganize | Visual browser: Move / Delete with aspect-ratio filter + rename dialog |
| 3 | AI Models | Manage the CLIP engine and the Ollama server connection |
| 4 | Categories | Create / edit / configure categories; AI Suggest from sample images |
| 5 | Tags | Edit the global tag registry |
| 6 | Duplicates | MD5 duplicate scan, review groups, Move / Delete actions |
| 7 | Dependencies | Install optional packages (OpenCV, scikit-learn, PySide6, ...) |
| 8 | Settings | Application configuration (thresholds, theme, destination folder) |
A typical session:
+-- Dashboard ---------------------------------------------------+
| Total Files: 12 348 Categories: 23 Duplicates: 87 |
| Mode: [x] Fusion (CLIP + LowLevel) [ ] CLIP ... |
| CLIP: Ready Ollama: Connected (4 models) |
| [ Open Organizer ] [ AI Models ] [ Refresh ] |
+--------------------------------------------------------------+
|
v
+-- Organize ----------------------------------------------------+
| Source: /home/.../Incoming |
| Destination: /home/.../WP |
| Mode: Fusion Workers: 8 Min quality: 0.0 |
| [x] Dry run [x] Find duplicates [ ] Full reset |
| [#############-----] 67 % image_0432.jpg -> Cyberpunk |
| [ Organize Now ] [ Dry Run ] [ Stop ] |
+--------------------------------------------------------------+
Wanalizer/
run.sh Entry point (GUI or CLI)
pyproject.toml Build config + tool settings (ruff, pytest)
requirements.txt pip-installable runtime hints
CHANGELOG.md Release notes
CONTRIBUTING.md Development guide
tags.json Global tag registry (editable copy)
wallpaper_analyzer/ Core package
__init__.py Project metadata
py.typed Type-checker marker
analyzers/ Pluggable analyzer modes
base.py Abstract BaseAnalyzer
lowlevel_mode.py Low-Level CV analyzer
fusion_mode.py CLIP + LowLevel fusion
lowlevel/ Classical CV primitives
edges.py Canny, Sobel, Scharr, orientation
silhouettes.py Otsu, adaptive thresholding
contours.py Hu moments, symmetry
texture.py LBP, GLCM, Gabor filters
features.py ORB, FAST, Shi-Tomasi
fourier.py FFT, frequency distribution
hog.py Histogram of Oriented Gradients
color_advanced.py Colour moments, harmony, LAB / HSV stats
composition.py Rule of thirds, depth, saliency
quality_advanced.py Tenengrad, BRISQUE-like, pHash, noise
subject.py Largest component, foreground / background
pattern.py Periodicity, tile detection, complexity
symmetry_advanced.py Bilateral, rotational, diagonal
category_profile.py Per-category CV fingerprints + scoring
gui/ PySide6 desktop UI
__init__.py Main window + theme
__main__.py `python -m wallpaper_analyzer.gui`
theme.py Red / black / white QSS stylesheet
widgets.py Custom widgets
workers.py Background QThread workers
rename_dialog.py Batch rename dialog
category_config_dialog.py Per-category Q&A wizard + AI review
pages/
dashboard.py Overview, mode selection, health
organize.py Classification pipeline runner
reorganize.py Visual file browser, filter, rename
ai_models.py CLIP and Ollama model management
categories.py Category folder management
tags.py Tag registry editor
duplicates.py Duplicate scan UI
dependencies.py Package installer
settings.py Configuration panel
tools/ One-off maintenance scripts
regenerate_categories.py Rebuild every category config via Ollama
data/ Bundled non-code assets
tags.json Default tag registry shipped with the wheel
clip_client.py CLIP engine and CLIPAnalyzer
ollama_client.py Ollama vision LLM analyzer
vision_api_client.py OpenAI-compatible vision API analyzer (OpenAI/OpenRouter/DeepSeek/...)
categories.py Category management
category_config.py Per-category expected-configuration system
classify.py Multi-signal classification combiner
clean_tags.py Tag cleanup utilities
color.py HSV / LAB / RGB helpers
duplicates.py MD5-based duplicate detection
formats.py Format detection (50+)
hashing.py Perceptual hashing helpers
minimal_ai.py Minimal AI tag / prompt generator
organize.py Main classification pipeline + factory
parallel.py Parallel processing helpers
profile.py Image profile builder
prompt_generator.py AI prompt generation
quality.py Sharpness, aesthetic scoring
rename.py Rename strategies
settings.py Configuration
tag_policies.py Tag policy enforcement
tag_suggester.py AI tag suggestion
tags.py Tag registry
cli.py Command-line interface
tests/ Pytest smoke tests
The launcher auto-creates .venv, installs PySide6, picks the right Python interpreter, and falls back to a free-threaded runtime when asked:
./run.sh # GUI
./run.sh --cli --help # CLI
./run.sh --bootstrap # install everything upfront
./run.sh --bootstrap-ft # install only free-threaded deps into .venv-tpython3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
# Optional: full CV support
pip install opencv-python-headless imagehash scikit-learn
# Optional: GUI
pip install PySide6
# Optional: CLIP + PyTorch (CPU build, ~2 GB)
pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
pip install ftfy regex
pip install git+https://github.com/openai/CLIP.git
# Or install everything from the extras
pip install ".[all]"After pip install ., two console scripts are available:
wanalyzer # CLI
wanalyzer-gui # GUI| Tool | Used for | Required? |
|---|---|---|
ffmpeg |
Single-frame extraction for videos | Optional - graceful skip if missing |
# Debian / Ubuntu
sudo apt install ffmpeg
# macOS
brew install ffmpegSettings can be overridden via environment variables. They take precedence over .wallpaper_analyzer.json but are overridden by explicit CLI flags.
| Variable | Equivalent setting key |
|---|---|
WANALIZER_DEST |
dest_dir |
WANALIZER_MODE |
organize_mode |
WANALIZER_THEME |
theme |
OLLAMA_URL |
ollama_url |
WANALIZER_OLLAMA_URL |
ollama_url |
OLLAMA_MODEL |
ollama_model |
WANALIZER_OLLAMA_MODEL |
ollama_model |
WANALIZER_CLIP_MODEL |
clip_model |
WANALIZER_TAGS |
path to a custom tags.json file |
QT_QPA_PLATFORM |
Qt platform plugin (wayland / xcb / offscreen) |
wanalyzer [-h] [--mode {lowlevel,clip,fusion,ollama,api}] [--dest DIR]
[--dry] [--full] [--flatten] [--find-duplicates]
[--dedupe | --no-dedupe] [--parallel N]
[--check-deps] [--list-modes]
[--set-dest DIR] [--show-config] [--report FILE]
| Flag | Description |
|---|---|
--mode, -m |
lowlevel (default), clip, fusion, ollama, api |
--dest, -d |
Destination folder (default WP/) |
--dry |
Preview only - do not move files |
--full, -f |
Flatten everything first, then reclassify |
--flatten |
Just flatten subdirectories |
--find-duplicates |
List duplicates without moving |
--dedupe / --no-dedupe |
Enable / disable duplicate removal (default: on) |
--parallel N |
Worker count (default: number of CPU cores) |
--check-deps |
Show optional dependency status |
--list-modes |
List analysis modes with descriptions |
--set-dest DIR |
Set destination folder and save to config |
--show-config |
Show current configuration |
--report FILE |
Save a JSON report |
./run.sh Launch GUI
./run.sh --cli [--ft] CLI mode (optionally free-threaded)
./run.sh --ft Force free-threaded Python (CLI fallback if no PySide6)
./run.sh --bootstrap Install all deps into .venv and .venv-t
./run.sh --bootstrap-ft Install only free-threaded deps into .venv-t
./run.sh --version Print launcher / project version
./run.sh --help Show launcher help
Preview without moving anything:
./run.sh --cli --mode fusion --dryForce a full re-classification (flatten first):
./run.sh --cli --full --mode fusionUse all your cores for hashing and classification:
./run.sh --cli --mode lowlevel --parallel 16Fusion / CLIP / Ollama modes automatically cap workers at 1 to avoid loading the model four times into RAM.
Point Wanalizer at a different library:
./run.sh --cli --set-dest /media/wallpapers/MainRun on a remote Ollama server:
OLLAMA_URL=http://gpu-box:11434 ./run.sh --cli --mode ollamaClassify via a cloud vision API (OpenAI-compatible — works with OpenAI, OpenRouter, Groq, LM Studio, self-hosted vLLM, or DeepSeek served through a compatible endpoint). For DeepSeek-VL / DeepSeek-VL2 use either:
- a self-hosted vLLM server (
vllm serve deepseek-ai/deepseek-vl2-small) pointed athttp://localhost:8000/v1, or - a gateway like OpenRouter that exposes the DeepSeek-VL2 family.
Pick the model in AI Models → Vision API, where a clickable table lists the curated DeepSeek identifiers:
| Key | HF id | Size | VRAM | Speed | Accuracy |
|---|---|---|---|---|---|
| DeepSeek-VL2-Tiny | deepseek-ai/deepseek-vl2-tiny |
2.0 GB | 5 GB | fast | good |
| DeepSeek-VL2-Small | deepseek-ai/deepseek-vl2-small |
8.5 GB | 18 GB | medium | high |
| DeepSeek-VL2 (Base) | deepseek-ai/deepseek-vl2 |
14 GB | 28 GB | slow | very high |
| DeepSeek-VL 7B | deepseek-ai/deepseek-vl-7b-chat |
14 GB | 16 GB | medium | high |
| DeepSeek-VL 1.3B | deepseek-ai/deepseek-vl-1.3b-chat |
2.8 GB | 4 GB | very fast | medium |
WANALIZER_VISION_API_URL=http://localhost:8000/v1 \
WANALIZER_VISION_API_MODEL=deepseek-ai/deepseek-vl2-small \
./run.sh --cli --mode api --dryThe same settings can be configured interactively in the GUI under
AI Models → Vision API, and saved into .wallpaper_analyzer.json.
Clean a polluted tag registry:
./run.sh --cli --mode lowlevel --dry
python -c "from wallpaper_analyzer.clean_tags import clean_all; clean_all()"Run the smoke tests after a change:
pip install ".[dev]"
pytest -qLint the codebase:
pip install ".[dev]"
ruff check .- MD5-only duplicate detection with cache (v3.0)
- CLIP + LowLevel fusion mode (v3.0)
- Free-threaded Python bootstrap (v3.0)
- Per-category AI Suggest + Q&A wizard (v3.0)
- Bundled
data/tags.jsonandpy.typedmarker (v3.1) - Unified
wallpaper_analyzer.analyzers.get_analyzerfactory (v3.1) - Environment-variable overrides for settings (v3.1)
- Optional perceptual-hash duplicate tier (pHash / dHash) as opt-in
- Plugin system for third-party analyzers
- Multi-library profiles (separate WP, Anime, Photos libraries)
- Optional GPU acceleration for CLIP via
--device cuda
Made for organized desktops everywhere.
