organize pics
This commit is contained in:
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