rename AI snaps improvements
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
|
/__pycache__
|
||||||
|
|||||||
136
rename-ai-snaps
136
rename-ai-snaps
@@ -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.")
|
||||||
|
|||||||
Reference in New Issue
Block a user