organize pics
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
83
extract-ai-meta
Executable file
83
extract-ai-meta
Executable file
@@ -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()
|
||||
235
organize-images
Executable file
235
organize-images
Executable file
@@ -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<filled; i++)); do printf "#"; done
|
||||
for ((i=0; i<empty; i++)); do printf "."; done
|
||||
printf "] %3d%% (%d/%d)" "$pct" "$current" "$total"
|
||||
}
|
||||
|
||||
ai_dest() {
|
||||
python3 -c "
|
||||
import json, sys
|
||||
m = json.load(open('$AI_META'))
|
||||
entry = m.get(sys.argv[1], {})
|
||||
if not entry:
|
||||
print('AI/unknown')
|
||||
sys.exit(0)
|
||||
model = entry.get('model', 'unknown')
|
||||
loras = entry.get('loras', [])
|
||||
if loras:
|
||||
combo = '+'.join(l.strip() for l in loras)
|
||||
combo = (combo[:200]).rstrip('+')
|
||||
print(f'AI/{model}/{combo}')
|
||||
else:
|
||||
print(f'AI/{model}/no-lora')
|
||||
" "$1"
|
||||
}
|
||||
|
||||
idx=0
|
||||
for f in "${FILES[@]}"; do
|
||||
idx=$((idx + 1))
|
||||
|
||||
cat=$(classify "$f")
|
||||
|
||||
if [ "$cat" = "AI" ] && [ "$FLAT" = false ] && [ -n "$AI_META" ]; then
|
||||
sub=$(ai_dest "$(basename "$f")")
|
||||
dest="$TARGET/$sub"
|
||||
else
|
||||
dest="$TARGET/$cat/"
|
||||
fi
|
||||
|
||||
if [ "$VERBOSE" = true ]; then
|
||||
if [ "$cat" = "AI" ] && [ "$FLAT" = false ] && [ -n "$AI_META" ]; then
|
||||
printf " (%d/%d) [%s] %s\n" "$idx" "$TOTAL" "${sub#AI/}" "$(basename "$f")"
|
||||
else
|
||||
printf " (%d/%d) [%s] %s\n" "$idx" "$TOTAL" "$cat" "$(basename "$f")"
|
||||
fi
|
||||
else
|
||||
progress_bar "$idx" "$TOTAL"
|
||||
fi
|
||||
|
||||
if [ "$DRY_RUN" = false ]; then
|
||||
mkdir -p "$dest"
|
||||
mv -n "$f" "$dest"
|
||||
fi
|
||||
|
||||
case "$cat" in
|
||||
AI) cAI=$((cAI + 1)) ;;
|
||||
Photos) cPhotos=$((cPhotos + 1)) ;;
|
||||
Screenshots) cScreenshots=$((cScreenshots + 1)) ;;
|
||||
Unsorted) cUnsorted=$((cUnsorted + 1)) ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$VERBOSE" = false ]; then
|
||||
printf "\r %-60s\n" "Done."
|
||||
fi
|
||||
|
||||
echo
|
||||
echo " Results:"
|
||||
if [ "$FLAT" = false ] && [ -n "$AI_META" ]; then
|
||||
echo " AI (by model/LoRA): $cAI"
|
||||
else
|
||||
echo " AI: $cAI"
|
||||
fi
|
||||
echo " Photos: $cPhotos"
|
||||
echo " Screenshots: $cScreenshots"
|
||||
echo " Unsorted: $cUnsorted"
|
||||
echo
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
echo " Dry run complete. Re-run with -e to execute."
|
||||
fi
|
||||
Reference in New Issue
Block a user