rename AI snaps improvements

This commit is contained in:
2026-06-01 11:02:57 -07:00
parent 8f2010d5a8
commit 7c2b155837
2 changed files with 108 additions and 29 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
.DS_Store .DS_Store
/__pycache__

View File

@@ -170,10 +170,10 @@ def extract_prompts(filepath):
return pos, neg return pos, neg
def extract_keywords(text, max_chars=40): def extract_all_candidates(text):
"""Generate a short dash-separated keyword description from prompt text.""" """Extract all candidate keywords with position index for frequency scoring."""
if not text: if not text:
return None return []
text_lower = text.lower() text_lower = text.lower()
@@ -183,9 +183,11 @@ def extract_keywords(text, max_chars=40):
# Split on commas, periods, semicolons, exclamation/question marks # Split on commas, periods, semicolons, exclamation/question marks
segments = re.split(r"[,.;:!?]+", cleaned) segments = re.split(r"[,.;:!?]+", cleaned)
# Collect meaningful keywords, preserving order # Collect meaningful keywords, preserving position order
seen = set() seen = set()
keywords = [] candidates = []
position = 0
for seg in segments: for seg in segments:
seg = seg.strip() seg = seg.strip()
if not seg or len(seg) < 4: if not seg or len(seg) < 4:
@@ -218,43 +220,94 @@ def extract_keywords(text, max_chars=40):
taken = 0 taken = 0
for g in good: for g in good:
if g not in seen and taken < 2: if g not in seen and taken < 2:
keywords.append(g) candidates.append((g, position))
seen.add(g) seen.add(g)
taken += 1 taken += 1
position += 1
return candidates
def select_best_keywords(candidates, freq_map, total_images, max_keywords=5):
"""Select most discriminating keywords by rarity and position.
Keywords that appear in fewer images across the batch score higher.
Later-position keywords get a small bonus for tiebreaking.
When total_images <= 1, falls back to first-N selection.
"""
if not candidates:
return []
if total_images <= 1:
return [kw for kw, _ in candidates[:max_keywords]]
scored = []
for kw, pos in candidates:
freq = freq_map.get(kw, 1)
# Rarity: 1.0 = appears in 1 image, 0.0 = appears in all images
rarity = 1.0 - ((freq - 1) / max(total_images - 1, 1))
# Position bonus: slight preference for later words (up to 0.3)
max_pos = max(len(candidates), 1)
pos_bonus = (pos / max_pos) * 0.3
score = rarity + pos_bonus
scored.append((score, kw, pos))
# Sort by score desc, then by original position asc for stability
scored.sort(key=lambda x: (-x[0], x[2]))
selected = []
seen = set()
for _, kw, _ in scored:
if kw not in seen:
selected.append(kw)
seen.add(kw)
if len(selected) >= max_keywords:
break
return selected
def keywords_to_name(keywords):
"""Convert keyword list to 'schmeeve-AI-{keywords}.png' name."""
if not keywords: if not keywords:
return None return None
# Build description: up to 5 keywords desc = "-".join(keywords)
desc = "-".join(keywords[:5])
# Replace any non-alphanumeric (except hyphen) with hyphens
desc = re.sub(r"[^a-z0-9-]", "-", desc) desc = re.sub(r"[^a-z0-9-]", "-", desc)
desc = re.sub(r"-+", "-", desc).strip("-") desc = re.sub(r"-+", "-", desc).strip("-")
# Truncate at max_chars, breaking at a word boundary # Truncate at 40 chars, breaking at a word boundary
if len(desc) > max_chars: if len(desc) > 40:
desc = desc[:max_chars].rstrip("-") desc = desc[:40].rstrip("-")
if max_chars > 10 and "-" in desc: if "-" in desc:
truncated = "-".join(desc.split("-")[:-1]) truncated = "-".join(desc.split("-")[:-1])
if truncated and len(truncated) > 10: if truncated and len(truncated) > 10:
desc = truncated desc = truncated
return desc if desc and len(desc) > 3 else None return f"schmeeve-AI-{desc}.png" if desc and len(desc) > 3 else None
def propose_name(filepath): def propose_name(filepath, freq_map=None, total_images=1):
"""Propose 'schmeeve-AI-{keywords}.png' or None.""" """Propose 'schmeeve-AI-{keywords}.png' or None.
When freq_map and total_images > 1 are provided, uses frequency-aware
keyword selection that favors rare (discriminating) words.
"""
pos, neg = extract_prompts(filepath) pos, neg = extract_prompts(filepath)
source = pos or neg source = pos or neg
if not source: if not source:
return None return None
desc = extract_keywords(source) candidates = extract_all_candidates(source)
if not desc: if not candidates:
return None return None
return f"schmeeve-AI-{desc}.png" if freq_map and total_images > 1:
keywords = select_best_keywords(candidates, freq_map, total_images)
else:
keywords = [kw for kw, _ in candidates[:5]]
return keywords_to_name(keywords)
# ── main ─────────────────────────────────────────────────────────────────── # ── main ───────────────────────────────────────────────────────────────────
@@ -282,40 +335,65 @@ def main():
pngs = sorted(scan_dir.glob("*.png")) pngs = sorted(scan_dir.glob("*.png"))
# ── Phase 1: Analyze with spinner ── # ── Phase 1: Collect candidates and build frequency map ──
sys.stdout.write(" Analyzing PNGs") sys.stdout.write(" Analyzing PNGs")
sys.stdout.flush() sys.stdout.flush()
spin = spinner() spin = spinner()
raw_proposals = {} image_data = {}
word_images = {}
for p in pngs: for p in pngs:
sys.stdout.write(f"\r {next(spin)} Analyzing PNGs") sys.stdout.write(f"\r {next(spin)} Analyzing PNGs")
sys.stdout.flush() sys.stdout.flush()
# Skip already-renamed files # Skip already-renamed files
if p.name.startswith("schmeeve-AI-"): if p.name.startswith("schmeeve-AI-"):
continue continue
name = propose_name(str(p)) pos, neg = extract_prompts(str(p))
source = pos or neg
if source:
candidates = extract_all_candidates(source)
if candidates:
image_data[p] = candidates
for kw, _ in candidates:
if kw not in word_images:
word_images[kw] = set()
word_images[kw].add(p)
time.sleep(0.02)
freq_map = {kw: len(images) for kw, images in word_images.items()}
total_images = len(image_data)
# ── Phase 1b: Propose names with frequency-aware selection ──
raw_proposals = {}
for p, candidates in image_data.items():
if total_images > 1:
keywords = select_best_keywords(candidates, freq_map, total_images)
else:
keywords = [kw for kw, _ in candidates[:5]]
name = keywords_to_name(keywords)
if name: if name:
raw_proposals[p] = name raw_proposals[p] = name
time.sleep(0.02)
# Clear the spinner line # Clear the spinner line
sys.stdout.write("\r" + " " * 60 + "\r") sys.stdout.write("\r" + " " * 60 + "\r")
sys.stdout.flush() sys.stdout.flush()
# Deduplicate proposed names # Deduplicate proposed names; use short mtime tag for collisions
proposals = {} proposals = {}
name_counts = {} name_counts = {}
for p, name in raw_proposals.items(): for p, name in raw_proposals.items():
base = name base = name
if base in name_counts: if base in name_counts:
name_counts[base] += 1 name_counts[base] += 1
stem = base.rsplit(".", 1)[0]
ext = ".png"
name = f"{stem}_{name_counts[base]}{ext}"
else: else:
name_counts[base] = 0 name_counts[base] = 1
proposals[p] = name proposals[p] = name
for p, name in list(proposals.items()):
if name_counts[name] > 1:
ts = time.strftime("%m%d-%H%M%S", time.localtime(p.stat().st_mtime))
stem = name.rsplit(".", 1)[0]
proposals[p] = f"{stem}-{ts}.png"
# ── Phase 2: Display proposed names ── # ── Phase 2: Display proposed names ──
if not proposals: if not proposals:
print(" No renamable PNGs found.") print(" No renamable PNGs found.")