From a808cd2cc7b4c91a059d768ef579e6f480dd57a2 Mon Sep 17 00:00:00 2001 From: schmeeve Date: Tue, 9 Jun 2026 10:17:55 -0700 Subject: [PATCH] organize pics --- AGENTS.md | 4 +- extract-ai-meta | 83 +++++++++++++++++ organize-images | 235 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 320 insertions(+), 2 deletions(-) create mode 100755 extract-ai-meta create mode 100755 organize-images diff --git a/AGENTS.md b/AGENTS.md index 8f1df0e..e33c467 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # schmeeve-toolz — Agent Guide -Personal macOS automation toolkit deployed to `~/Dropbox/bin/`. Mostly shell scripts for power management, network, and system automation, triggered by Keyboard Maestro and cron. +Personal macOS automation toolkit. Newer scripts deploy to `~/.local/bin/`; older scripts live in `~/Dropbox/bin/`. Mostly shell scripts for power management, network, and system automation, triggered by Keyboard Maestro and cron. ## Commands @@ -11,7 +11,7 @@ No build/test/deploy infrastructure exists. Scripts run directly: chmod +x scriptname # scripts should already be executable ``` -Scripts are deployed by copying (or symlinking) to `~/Dropbox/bin/`. There is no install step. +Scripts are deployed by copying (or symlinking) to `~/.local/bin/` (newer) or `~/Dropbox/bin/` (legacy). There is no install step. Check shebang (`#!/bin/sh` or `#!/bin/bash`) before running — some use bashisms like `[[ ]]` and arrays. diff --git a/extract-ai-meta b/extract-ai-meta new file mode 100755 index 0000000..6d46ad8 --- /dev/null +++ b/extract-ai-meta @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +"""Extract model and LoRA metadata from ComfyUI-generated images. + +Outputs a JSON mapping of filename → {"model": "...", "loras": ["...", ...]}. +""" + +import json +import os +import sys +from pathlib import Path + +try: + from PIL import Image +except ImportError: + print("Error: Pillow (PIL) required. Install with: pip install Pillow", file=sys.stderr) + sys.exit(1) + +IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".webp"} + + +def extract_ai_meta(filepath): + """Return (model, loras) or (None, []) if no ComfyUI metadata found.""" + try: + img = Image.open(filepath) + except Exception: + return None, [] + + if "prompt" not in img.info: + return None, [] + + try: + data = json.loads(img.info["prompt"]) + except (json.JSONDecodeError, TypeError): + return None, [] + + model = None + loras = [] + + for node in data.values(): + cls = node.get("class_type", "") + inputs = node.get("inputs", {}) + + if cls == "CheckpointLoaderSimple": + ckpt = inputs.get("ckpt_name", "") + if ckpt: + model = ckpt.rsplit(".", 1)[0] + + if "lora" in cls.lower(): + lora = inputs.get("lora_name", "") + if lora: + loras.append(lora.rsplit(".", 1)[0]) + + loras.sort() + return model, loras + + +def main(): + target = sys.argv[1] if len(sys.argv) > 1 else os.path.expanduser("~/Pictures") + target = os.path.abspath(target) + + if not os.path.isdir(target): + print(f"Error: {target} is not a directory", file=sys.stderr) + sys.exit(1) + + meta = {} + + for entry in sorted(os.listdir(target)): + fpath = os.path.join(target, entry) + if not os.path.isfile(fpath): + continue + ext = os.path.splitext(entry)[1].lower() + if ext not in IMAGE_EXTS: + continue + + model, loras = extract_ai_meta(fpath) + if model or loras: + meta[entry] = {"model": model, "loras": loras} + + print(json.dumps(meta, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/organize-images b/organize-images new file mode 100755 index 0000000..b79831d --- /dev/null +++ b/organize-images @@ -0,0 +1,235 @@ +#!/bin/bash +# organize-images -- Sort images into AI/, Photos/, Screenshots/, Unsorted/ +# Default: sub-categorizes AI by model + LoRA combo (uses extract-ai-meta). +# Use --flat for one flat AI/ folder. + +TARGET="" +DRY_RUN=true +VERBOSE=false +INSTALL=false +FLAT=false + +usage() { + echo "Usage: $(basename "$0") [path] [-e|--execute] [-v|--verbose] [-i|--install] [--flat]" + echo " path Directory to organize (default: ~/Pictures)" + echo " -e Execute moves (default: dry-run only)" + echo " -v Verbose: show per-file classification" + echo " -i Install this script to ~/.local/bin/" + echo " --flat Don't sub-categorize by model/LoRA (flat AI/ folder)" + exit 0 +} + +ARGS=() +for arg in "$@"; do + case "$arg" in + -e|--execute) DRY_RUN=false ;; + -v|--verbose) VERBOSE=true ;; + -i|--install) INSTALL=true ;; + --flat) FLAT=true ;; + -h|--help) usage ;; + *) ARGS+=("$arg") ;; + esac +done + +if [ "$INSTALL" = true ]; then + SRC="$(cd "$(dirname "$0")" && pwd -P)/$(basename "$0")" + DEST="$HOME/.local/bin/$(basename "$0")" + mkdir -p "$HOME/.local/bin" + if [ "$SRC" = "$DEST" ]; then + echo "Already installed at $DEST" + else + cp "$SRC" "$DEST" + chmod +x "$DEST" + echo "Installed to $DEST" + fi + exit 0 +fi + +TARGET="${ARGS[0]:-$HOME/Pictures}" +TARGET="${TARGET%/}" + +if [ ! -d "$TARGET" ]; then + echo "Error: '$TARGET' is not a directory" >&2 + exit 1 +fi + +echo "organize-images — Classifying images in: $TARGET" +[ "$DRY_RUN" = true ] && echo " DRY RUN (use -e to execute)" || echo " EXECUTING" + +# --- Pre-scan: extract AI metadata for model/LoRA routing --- +AI_META="" +if [ "$FLAT" = false ]; then + META_SCRIPT="$(cd "$(dirname "$0")" && pwd -P)/extract-ai-meta" + if [ -x "$META_SCRIPT" ]; then + echo " Scanning AI metadata for model/LoRA routing..." + AI_META=$(mktemp) + if ! "$META_SCRIPT" "$TARGET" > "$AI_META" 2>/dev/null; then + echo " Warning: AI metadata scan failed, falling back to flat AI/" + FLAT=true + rm -f "$AI_META" + elif [ ! -s "$AI_META" ]; then + FLAT=true + rm -f "$AI_META" + fi + else + echo " Warning: extract-ai-meta not found, falling back to flat AI/" + FLAT=true + fi +fi + +[ "$FLAT" = false ] && echo " AI sub-categorization: by model/LoRA (use --flat to disable)" +echo + +classify() { + local f="$1" + + local name + name=$(basename "$f") + case "$name" in + CleanShot*|Screen_Shot*|Screenshot*) echo "Screenshots"; return ;; + esac + + local comment + comment=$(mdls -name kMDItemComment "$f" 2>/dev/null) + if echo "$comment" | grep -q '= "Screenshot"'; then + echo "Screenshots"; return + fi + + case "$name" in + ComfyUI_*|*_DEFI_*|R.*.png|schmeeve-AI-*) echo "AI"; return ;; + esac + + if mdls -name kMDItemAcquisitionMake "$f" 2>/dev/null | grep -q '= "'; then + echo "Photos"; return + fi + + local ext="${f##*.}" + ext=$(echo "$ext" | tr '[:upper:]' '[:lower:]') + case "$ext" in + dng|cr2|nef|arw|orf|rw2|raf|3fr) + echo "Photos"; return ;; + esac + + if [ "$ext" = "png" ]; then + local dims w h area ratio + dims=$(sips --getProperty pixelWidth --getProperty pixelHeight "$f" 2>/dev/null | grep -E 'pixel(Width|Height)' | awk '{print $NF}') + w=$(echo "$dims" | head -1) + h=$(echo "$dims" | tail -1) + if [ -n "$w" ] && [ -n "$h" ] && [ "$w" -gt 0 ] && [ "$h" -gt 0 ]; then + area=$((w * h)) + ratio=$(echo "scale=4; $w / $h" | bc 2>/dev/null || echo 1) + if [ "$area" -gt 1048576 ] && [ "$(echo "$ratio > 0.75" | bc -l)" = 1 ] && [ "$(echo "$ratio < 1.33" | bc -l)" = 1 ]; then + echo "AI"; return + fi + fi + fi + + echo "Unsorted" +} + +# --- Pre-scan: collect all image files --- +FILES=() +for f in "$TARGET"/*; do + [ -f "$f" ] || continue + ext="${f##*.}"; ext=$(echo "$ext" | tr '[:upper:]' '[:lower:]') + case "$ext" in + jpg|jpeg|png|webp|gif|bmp|tiff|tif|heic|heif|dng|cr2|nef|arw|orf|rw2|raf|3fr) + FILES+=("$f") ;; + esac +done +TOTAL=${#FILES[@]} +if [ "$TOTAL" -eq 0 ]; then + echo " No image files found." + exit 0 +fi + +mkdir -p "$TARGET/AI" "$TARGET/Photos" "$TARGET/Screenshots" "$TARGET/Unsorted" + +cAI=0; cPhotos=0; cScreenshots=0; cUnsorted=0 + +progress_bar() { + local current=$1 total=$2 w=40 + local pct=0 filled=0 empty=0 + [ "$total" -gt 0 ] && pct=$((current * 100 / total)) + [ "$total" -gt 0 ] && filled=$((current * w / total)) + empty=$((w - filled)) + printf "\r [" + for ((i=0; i