From 7c2b155837cc4bf1e992287d86324809d4ab8f18 Mon Sep 17 00:00:00 2001 From: schmeeve Date: Mon, 1 Jun 2026 11:02:57 -0700 Subject: [PATCH] rename AI snaps improvements --- .gitignore | 1 + rename-ai-snaps | 136 +++++++++++++++++++++++++++++++++++++----------- 2 files changed, 108 insertions(+), 29 deletions(-) diff --git a/.gitignore b/.gitignore index e43b0f9..9369599 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .DS_Store +/__pycache__ diff --git a/rename-ai-snaps b/rename-ai-snaps index e4b9768..0fac20b 100755 --- a/rename-ai-snaps +++ b/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.")