rename AI snaps improvements
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
.DS_Store
|
||||
/__pycache__
|
||||
|
||||
136
rename-ai-snaps
136
rename-ai-snaps
@@ -170,10 +170,10 @@ def extract_prompts(filepath):
|
||||
return pos, neg
|
||||
|
||||
|
||||
def extract_keywords(text, max_chars=40):
|
||||
"""Generate a short dash-separated keyword description from prompt text."""
|
||||
def extract_all_candidates(text):
|
||||
"""Extract all candidate keywords with position index for frequency scoring."""
|
||||
if not text:
|
||||
return None
|
||||
return []
|
||||
|
||||
text_lower = text.lower()
|
||||
|
||||
@@ -183,9 +183,11 @@ def extract_keywords(text, max_chars=40):
|
||||
# Split on commas, periods, semicolons, exclamation/question marks
|
||||
segments = re.split(r"[,.;:!?]+", cleaned)
|
||||
|
||||
# Collect meaningful keywords, preserving order
|
||||
# Collect meaningful keywords, preserving position order
|
||||
seen = set()
|
||||
keywords = []
|
||||
candidates = []
|
||||
position = 0
|
||||
|
||||
for seg in segments:
|
||||
seg = seg.strip()
|
||||
if not seg or len(seg) < 4:
|
||||
@@ -218,43 +220,94 @@ def extract_keywords(text, max_chars=40):
|
||||
taken = 0
|
||||
for g in good:
|
||||
if g not in seen and taken < 2:
|
||||
keywords.append(g)
|
||||
candidates.append((g, position))
|
||||
seen.add(g)
|
||||
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:
|
||||
return None
|
||||
|
||||
# Build description: up to 5 keywords
|
||||
desc = "-".join(keywords[:5])
|
||||
|
||||
# Replace any non-alphanumeric (except hyphen) with hyphens
|
||||
desc = "-".join(keywords)
|
||||
desc = re.sub(r"[^a-z0-9-]", "-", desc)
|
||||
desc = re.sub(r"-+", "-", desc).strip("-")
|
||||
|
||||
# Truncate at max_chars, breaking at a word boundary
|
||||
if len(desc) > max_chars:
|
||||
desc = desc[:max_chars].rstrip("-")
|
||||
if max_chars > 10 and "-" in desc:
|
||||
# Truncate at 40 chars, breaking at a word boundary
|
||||
if len(desc) > 40:
|
||||
desc = desc[:40].rstrip("-")
|
||||
if "-" in desc:
|
||||
truncated = "-".join(desc.split("-")[:-1])
|
||||
if truncated and len(truncated) > 10:
|
||||
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):
|
||||
"""Propose 'schmeeve-AI-{keywords}.png' or None."""
|
||||
def propose_name(filepath, freq_map=None, total_images=1):
|
||||
"""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)
|
||||
source = pos or neg
|
||||
if not source:
|
||||
return None
|
||||
|
||||
desc = extract_keywords(source)
|
||||
if not desc:
|
||||
candidates = extract_all_candidates(source)
|
||||
if not candidates:
|
||||
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 ───────────────────────────────────────────────────────────────────
|
||||
@@ -282,40 +335,65 @@ def main():
|
||||
|
||||
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.flush()
|
||||
spin = spinner()
|
||||
raw_proposals = {}
|
||||
image_data = {}
|
||||
word_images = {}
|
||||
for p in pngs:
|
||||
sys.stdout.write(f"\r {next(spin)} Analyzing PNGs")
|
||||
sys.stdout.flush()
|
||||
# Skip already-renamed files
|
||||
if p.name.startswith("schmeeve-AI-"):
|
||||
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:
|
||||
raw_proposals[p] = name
|
||||
time.sleep(0.02)
|
||||
|
||||
# Clear the spinner line
|
||||
sys.stdout.write("\r" + " " * 60 + "\r")
|
||||
sys.stdout.flush()
|
||||
|
||||
# Deduplicate proposed names
|
||||
# Deduplicate proposed names; use short mtime tag for collisions
|
||||
proposals = {}
|
||||
name_counts = {}
|
||||
for p, name in raw_proposals.items():
|
||||
base = name
|
||||
if base in name_counts:
|
||||
name_counts[base] += 1
|
||||
stem = base.rsplit(".", 1)[0]
|
||||
ext = ".png"
|
||||
name = f"{stem}_{name_counts[base]}{ext}"
|
||||
else:
|
||||
name_counts[base] = 0
|
||||
name_counts[base] = 1
|
||||
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 ──
|
||||
if not proposals:
|
||||
print(" No renamable PNGs found.")
|
||||
|
||||
Reference in New Issue
Block a user