organize pics
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# schmeeve-toolz — Agent Guide
|
# 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
|
## Commands
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ No build/test/deploy infrastructure exists. Scripts run directly:
|
|||||||
chmod +x scriptname # scripts should already be executable
|
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.
|
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