import os
import re
import threading
import tkinter as tk
from tkinter import filedialog
import customtkinter as ctk
import ollama
from PIL import Image
# Set the visual theme
ctk.set_appearance_mode("dark")
ctk.set_default_color_theme("blue")
class RenamerApp(ctk.CTk):
def __init__(self):
super().__init__()
# Window Configuration
self.title("AI Image Renamer (with Resolution)")
self.geometry("600x480")
# --- UI Elements ---
self.label = ctk.CTkLabel(self, text="Ollama Image Renamer", font=ctk.CTkFont(size=24, weight="bold"))
self.label.pack(pady=(20, 10))
self.desc_label = ctk.CTkLabel(self, text="Renames images with AI description + resolution", font=ctk.CTkFont(size=13))
self.desc_lar = self.desc_label.pack(pady=(0, 20))
self.progress = ctk.CTkProgressBar(self, width=500)
self.progress.set(0)
self.progress.pack(pady=10)
self.log_area = ctk.CTkTextbox(self, width=540, height=200, font=ctk.CTkFont(family="Consolas", size=12))
self.log_area.pack(pady=10, padx=20)
self.log_area.configure(state="disabled")
self.btn_select = ctk.CTkButton(self, text="Select Images & Start", command=self.start_process_thread,
width=200, height=40, font=ctk.CTkFont(size=15, weight="bold"))
self.btn_select.pack(pady=20)
# Make sure this matches your installed model name exactly!
self.MODEL_NAME = "gemma4:26b"
def log(self, message):
self.log_area.configure(state="normal")
self.log_area.insert("end", message + "\n")
self.log_area.see("end")
self.log_area.configure(state="disabled")
print(message)
def sanitize_filename(self, name, max_length=50):
name = re.sub(r'[^\w\s-]', '', name.lower())
name = re.sub(r'[\s_]+', '-', name)
name = re.sub(r'-+', '-', name).strip('-')
return name[:max_length]
def start_process_thread(self):
file_paths = filedialog.askopenfilenames(
title="Select Images to Rename",
filetypes=[("Image Files", "*.jpg *.jpeg *.png *.webp *.bmp")]
)
if not file_paths:
return
self.btn_select.configure(state="disabled")
self.log_area.configure(state="normal")
self.log_area.delete("1.0", "end")
self.log_area.configure(state="disabled")
thread = threading.Thread(target=self.process_images, args=(file_paths,), daemon=True)
thread.start()
def process_images(self, file_paths):
total_files = len(file_paths)
self.log(f"Starting processing of {total_files} files...")
for i, old_path in enumerate(file_paths):
try:
filename = os.path.basename(old_path)
self.log(f"Analyzing: {filename}")
# Get Resolution using Pillow
with Image.open(old_path) as img:
width, height = img.size
res_str = f"{width}x{height}"
# Prepare bytes for Ollama
with open(old_path, 'rb') as f:
img_bytes = f.read()
prompt = (
"Describe the main subject of this image in 2 to 5 words. "
"Return ONLY the words separated by hyphens. No spaces, no special characters."
)
# Call Ollama
response = ollama.generate(
model=self.MODEL_NAME,
prompt=prompt,
images=[img_bytes]
)
ai_output = response['response'].strip()
# Clean the AI name
safe_name = self.sanitize_filename(ai_output)
if not safe_name:
safe_name = "unnamed-image"
# Combine Name + Resolution
final_base_name = f"{safe_name} {res_str}"
directory = os.path.dirname(old_path)
ext = os.path.splitext(old_path)[1].lower()
new_path = os.path.join(directory, f"{final_base_name}{ext}")
# Collision handling
counter = 1
while os.path.exists(new_path) and new_path != old_path:
new_path = os.path.join(directory, f"{final_base_name}-{counter}{ext}")
counter += 1
if old_path != new_path:
os.rename(old_path, new_path)
self.log(f" -> Renamed to: {os.path.basename(new_path)}")
else:
self.log(" -> Already correctly named.")
except Exception as e:
self.log(f" -> ERROR: {str(e)}")
# Update Progress Bar
progress_val = (i + 1) / total_files
self.progress.set(progress_val)
self.log("\n--- ALL TASKS COMPLETE ---")
self.btn_select.configure(state="normal")
if __name__ == "__main__":
app = RenamerApp()
app.mainloop()