1083 lines
46 KiB
Python
Raw Permalink Normal View History

2025-09-23 13:59:06 +01:00
#!/usr/bin/env python3
"""
Standalone FM NewGens - GUI Interface
A user-friendly graphical interface for generating Football Manager faces
"""
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
import os
import sys
import json
import configparser
import threading
import queue
import time
from pathlib import Path
from PIL import Image, ImageTk
import logging
# Import the main generation logic
2025-09-23 15:33:01 +01:00
from comfy_fm_newgen import generate_image, get_country_name, generate_prompts_for_players, get_prompt_for_image
2025-09-23 13:59:06 +01:00
try:
from lib.rtf_parser import RTF_Parser
except ImportError:
# Fallback for when striprtf is not available
class RTF_Parser:
def parse_rtf(self, file_path):
print(f"Warning: RTF parsing not available. Please install striprtf: pip install striprtf==0.0.28")
return []
2025-09-23 14:10:16 +01:00
# Check if background removal is available
try:
from lib.remove_bg import remove_bg_from_file_list, REMBG_AVAILABLE
BG_REMOVAL_AVAILABLE = REMBG_AVAILABLE
except ImportError:
BG_REMOVAL_AVAILABLE = False
print("Warning: Background removal not available")
2025-09-23 13:59:06 +01:00
from lib.generate_xml import create_config_xml, append_to_config_xml
from lib.xml_reader import extract_from_values
2025-09-23 14:30:05 +01:00
from lib.general import list_profiles
2025-09-23 13:59:06 +01:00
class FMFaceGeneratorGUI:
def __init__(self, root):
self.root = root
self.root.title("FM Face Generator - Standalone Version")
self.root.geometry("1000x700")
self.root.minsize(800, 600)
2025-09-23 14:25:59 +01:00
# Set environment variable to indicate GUI mode
os.environ['FM_NEWGEN_GUI_MODE'] = 'true'
2025-09-23 13:59:06 +01:00
# Initialize variables
self.config = None
self.app_config = None
self.rtf = RTF_Parser()
self.generation_thread = None
self.stop_generation = False
self.image_queue = queue.Queue()
2025-09-23 14:25:59 +01:00
self.current_profile = None
2025-09-23 13:59:06 +01:00
2025-09-23 14:52:52 +01:00
# Preview tracking variables
self.generated_images = [] # List of generated image UIDs
self.current_image_index = -1 # Current image being displayed
2025-09-23 13:59:06 +01:00
# Set up the GUI
self.setup_gui()
2025-09-23 14:25:59 +01:00
# Show profile selection dialog first
self.show_profile_selection()
2025-09-23 13:59:06 +01:00
2025-09-23 14:28:56 +01:00
# Refresh system information
self.refresh_system_info()
2025-09-23 13:59:06 +01:00
# Start image preview update loop
self.update_image_preview()
2025-09-23 14:25:59 +01:00
def show_profile_selection(self):
"""Show profile selection dialog at startup"""
# Load existing profiles
profiles = self.list_profiles()
if not profiles:
# No profiles exist, create default one
self.create_default_profile()
self.current_profile = "NewGens"
self.load_config()
return
# Create selection dialog
dialog = tk.Toplevel(self.root)
dialog.title("Select Profile")
dialog.geometry("400x300")
dialog.transient(self.root)
dialog.grab_set()
# Center the dialog
dialog.update_idletasks()
x = (dialog.winfo_screenwidth() - dialog.winfo_width()) // 2
y = (dialog.winfo_screenheight() - dialog.winfo_height()) // 2
dialog.geometry(f"+{x}+{y}")
# Profile selection
ttk.Label(dialog, text="Select a profile to use:").pack(pady=10)
profile_var = tk.StringVar()
profile_combo = ttk.Combobox(dialog, textvariable=profile_var, width=30)
profile_combo['values'] = profiles
profile_combo.pack(pady=10)
profile_combo.set(profiles[0]) # Select first profile by default
def on_select():
selected = profile_var.get()
if selected:
self.current_profile = selected
dialog.destroy()
self.load_config()
self.log_message(f"Selected profile: {selected}")
def on_create_new():
dialog.destroy()
self.show_create_profile_dialog()
def on_manage():
dialog.destroy()
self.notebook.select(1) # Switch to profiles tab
# Buttons
btn_frame = ttk.Frame(dialog)
btn_frame.pack(pady=20)
ttk.Button(btn_frame, text="Select", command=on_select).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="Create New", command=on_create_new).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="Manage Profiles", command=on_manage).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="Cancel", command=self.root.quit).pack(side=tk.LEFT, padx=5)
# Wait for dialog to close
self.root.wait_window(dialog)
def show_create_profile_dialog(self):
"""Show dialog to create a new profile"""
dialog = tk.Toplevel(self.root)
dialog.title("Create New Profile")
dialog.geometry("400x250")
dialog.transient(self.root)
dialog.grab_set()
# Center the dialog
dialog.update_idletasks()
x = (dialog.winfo_screenwidth() - dialog.winfo_width()) // 2
y = (dialog.winfo_screenheight() - dialog.winfo_height()) // 2
dialog.geometry(f"+{x}+{y}")
# Profile name
ttk.Label(dialog, text="Profile Name:").grid(row=0, column=0, sticky=tk.W, pady=10, padx=10)
profile_name_var = tk.StringVar()
profile_name_entry = ttk.Entry(dialog, textvariable=profile_name_var, width=30)
profile_name_entry.grid(row=0, column=1, sticky=tk.W, padx=10, pady=10)
# FM Version
ttk.Label(dialog, text="FM Version:").grid(row=1, column=0, sticky=tk.W, pady=5, padx=10)
fm_version_var = tk.StringVar(value="2024")
fm_version_entry = ttk.Entry(dialog, textvariable=fm_version_var, width=30)
fm_version_entry.grid(row=1, column=1, sticky=tk.W, padx=10, pady=5)
# RTF File
ttk.Label(dialog, text="RTF File:").grid(row=2, column=0, sticky=tk.W, pady=5, padx=10)
rtf_file_var = tk.StringVar()
rtf_entry = ttk.Entry(dialog, textvariable=rtf_file_var, width=30)
rtf_entry.grid(row=2, column=1, sticky=tk.W, padx=10, pady=5)
ttk.Button(dialog, text="Browse...", command=lambda: self.browse_file(rtf_file_var, "RTF files", "*.rtf")).grid(row=2, column=2, padx=10, pady=5)
# Output Directory
ttk.Label(dialog, text="Output Directory:").grid(row=3, column=0, sticky=tk.W, pady=5, padx=10)
output_dir_var = tk.StringVar()
output_entry = ttk.Entry(dialog, textvariable=output_dir_var, width=30)
output_entry.grid(row=3, column=1, sticky=tk.W, padx=10, pady=5)
ttk.Button(dialog, text="Browse...", command=lambda: self.browse_directory(output_dir_var)).grid(row=3, column=2, padx=10, pady=5)
def on_create():
name = profile_name_var.get().strip()
if not name:
messagebox.showerror("Error", "Profile name cannot be empty")
return
if self.profile_exists(name):
messagebox.showerror("Error", f"Profile '{name}' already exists")
return
# Create the profile
self.create_profile(name, fm_version_var.get(), rtf_file_var.get(), output_dir_var.get())
self.current_profile = name
dialog.destroy()
self.load_config()
self.log_message(f"Created and selected profile: {name}")
def on_cancel():
dialog.destroy()
self.show_profile_selection() # Go back to profile selection
# Buttons
btn_frame = ttk.Frame(dialog)
btn_frame.grid(row=4, column=0, columnspan=3, pady=20)
ttk.Button(btn_frame, text="Create", command=on_create).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="Cancel", command=on_cancel).pack(side=tk.LEFT, padx=5)
def list_profiles(self):
"""List all available profiles"""
if not os.path.exists('user_config.cfg'):
return []
config = configparser.ConfigParser()
config.read('user_config.cfg')
profiles = [section.split(':', 1)[1] for section in config.sections() if section.startswith('profile:')]
return profiles
def profile_exists(self, profile_name):
"""Check if a profile exists"""
profiles = self.list_profiles()
return profile_name in profiles
def create_profile(self, name, fm_version, rtf_file, output_dir):
"""Create a new profile"""
if not self.config:
self.config = configparser.ConfigParser()
self.config.add_section('models')
section_name = f'profile:{name}'
self.config.add_section(section_name)
self.config.set(section_name, 'football_manager_version', fm_version)
self.config.set(section_name, 'rtf_file', rtf_file)
self.config.set(section_name, 'output_dir', output_dir)
# Save to file
with open('user_config.cfg', 'w') as configfile:
self.config.write(configfile)
def create_default_profile(self):
"""Create a default profile if none exists"""
self.config = configparser.ConfigParser()
self.config.add_section('models')
self.config.add_section('profile:NewGens')
self.config.set('models', 'model_name', 'SG161222/Realistic_Vision_V6.0_B1')
self.config.set('profile:NewGens', 'football_manager_version', '2024')
self.config.set('profile:NewGens', 'rtf_file', '')
self.config.set('profile:NewGens', 'output_dir', './NewGens/')
with open('user_config.cfg', 'w') as configfile:
self.config.write(configfile)
def browse_file(self, var, title, filetypes):
"""Browse for a file"""
file_path = filedialog.askopenfilename(title=title, filetypes=[(title, filetypes), ("All files", "*.*")])
if file_path:
var.set(file_path)
def browse_directory(self, var):
"""Browse for a directory"""
directory = filedialog.askdirectory(title="Select Directory")
if directory:
var.set(directory)
def setup_profiles_tab(self):
"""Set up the profiles management tab"""
# Profile list
list_frame = ttk.LabelFrame(self.profiles_frame, text="Available Profiles", padding=10)
list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
# Profile listbox
self.profiles_listbox = tk.Listbox(list_frame, height=10)
self.profiles_listbox.pack(fill=tk.BOTH, expand=True, pady=5)
# Buttons
btn_frame = ttk.Frame(list_frame)
btn_frame.pack(fill=tk.X, pady=5)
ttk.Button(btn_frame, text="Refresh List", command=self.refresh_profiles_list).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="Select Profile", command=self.select_profile_from_list).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="Create New", command=self.show_create_profile_dialog).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="Delete", command=self.delete_profile).pack(side=tk.RIGHT, padx=5)
# Profile details
details_frame = ttk.LabelFrame(self.profiles_frame, text="Profile Details", padding=10)
details_frame.pack(fill=tk.X, padx=10, pady=5)
# Current profile display
self.current_profile_var = tk.StringVar()
ttk.Label(details_frame, text="Current Profile:").grid(row=0, column=0, sticky=tk.W, pady=5)
ttk.Label(details_frame, textvariable=self.current_profile_var).grid(row=0, column=1, sticky=tk.W, padx=5, pady=5)
# Refresh the profiles list
self.refresh_profiles_list()
def refresh_profiles_list(self):
"""Refresh the profiles listbox"""
self.profiles_listbox.delete(0, tk.END)
profiles = self.list_profiles()
for profile in profiles:
self.profiles_listbox.insert(tk.END, profile)
if self.current_profile:
self.current_profile_var.set(self.current_profile)
2025-09-23 14:28:56 +01:00
def refresh_system_info(self):
"""Refresh and display system information"""
try:
import torch
# PyTorch version
self.pytorch_version_var.set(torch.__version__)
# CUDA availability
cuda_available = torch.cuda.is_available()
self.cuda_available_var.set("Yes" if cuda_available else "No")
# Check CUDA availability
if cuda_available:
gpu_count = torch.cuda.device_count()
current_device = torch.cuda.current_device()
gpu_name = torch.cuda.get_device_name(current_device)
gpu_memory = torch.cuda.get_device_properties(current_device).total_memory / 1024**3 # GB
self.gpu_status_var.set(f"Available ({gpu_name})")
self.gpu_memory_var.set(f"{gpu_memory:.1f} GB")
# Check if forcing CPU
if self.force_cpu_var.get() == "true":
self.gpu_status_var.set(f"Available but disabled (CPU forced)")
self.log_message(f"GPU available: {gpu_name} ({gpu_memory:.1f} GB) - CPU mode forced")
else:
self.log_message(f"GPU available: {gpu_name} ({gpu_memory:.1f} GB)")
else:
self.gpu_status_var.set("Not available (using CPU)")
self.gpu_memory_var.set("N/A")
self.log_message("GPU not available - using CPU. Install CUDA for GPU support.")
except ImportError:
self.gpu_status_var.set("PyTorch not available")
self.gpu_memory_var.set("N/A")
self.pytorch_version_var.set("Not installed")
self.cuda_available_var.set("N/A")
self.log_message("PyTorch not available - cannot detect GPU status")
except Exception as e:
self.gpu_status_var.set("Error detecting GPU")
self.gpu_memory_var.set("N/A")
self.pytorch_version_var.set("Error")
self.cuda_available_var.set("Error")
self.log_message(f"Error detecting GPU: {str(e)}")
def update_gpu_settings(self):
"""Update GPU settings when force CPU option changes"""
force_cpu = self.force_cpu_var.get() == "true"
os.environ['FM_NEWGEN_FORCE_CPU'] = 'true' if force_cpu else 'false'
if force_cpu:
self.log_message("GPU usage disabled - will use CPU")
else:
self.log_message("GPU usage enabled (if available)")
self.refresh_system_info() # Refresh to show actual status
def update_gpu_memory_usage(self):
"""Update GPU memory usage display during generation"""
try:
import torch
if torch.cuda.is_available() and not self.force_cpu_var.get() == "true":
# Get current GPU memory usage
if torch.cuda.is_available():
allocated = torch.cuda.memory_allocated(0) / 1024**3 # GB
reserved = torch.cuda.memory_reserved(0) / 1024**3 # GB
total = torch.cuda.get_device_properties(0).total_memory / 1024**3 # GB
self.gpu_usage_var.set(f"GPU Memory: {allocated:.1f}GB / {total:.1f}GB (Reserved: {reserved:.1f}GB)")
else:
self.gpu_usage_var.set("GPU Memory: N/A")
else:
self.gpu_usage_var.set("GPU Memory: CPU Mode")
except:
self.gpu_usage_var.set("GPU Memory: N/A")
# Schedule next update if generation is running
if hasattr(self, 'generation_thread') and self.generation_thread and self.generation_thread.is_alive():
self.root.after(2000, self.update_gpu_memory_usage) # Update every 2 seconds during generation
2025-09-23 14:25:59 +01:00
def select_profile_from_list(self):
"""Select a profile from the list"""
selection = self.profiles_listbox.curselection()
if selection:
profile_name = self.profiles_listbox.get(selection[0])
self.current_profile = profile_name
self.load_config()
self.log_message(f"Selected profile: {profile_name}")
self.refresh_profiles_list()
def delete_profile(self):
"""Delete the selected profile"""
selection = self.profiles_listbox.curselection()
if not selection:
messagebox.showerror("Error", "Please select a profile to delete")
return
profile_name = self.profiles_listbox.get(selection[0])
if profile_name == self.current_profile:
messagebox.showerror("Error", "Cannot delete the currently active profile")
return
if messagebox.askyesno("Confirm Delete", f"Are you sure you want to delete profile '{profile_name}'?"):
# Remove from config
if self.config:
self.config.remove_section(f'profile:{profile_name}')
with open('user_config.cfg', 'w') as configfile:
self.config.write(configfile)
self.log_message(f"Deleted profile: {profile_name}")
self.refresh_profiles_list()
2025-09-23 13:59:06 +01:00
def setup_gui(self):
"""Set up the main GUI components"""
# Create main notebook (tabs)
self.notebook = ttk.Notebook(self.root)
self.notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# Configuration tab
self.config_frame = ttk.Frame(self.notebook)
self.notebook.add(self.config_frame, text="Configuration")
2025-09-23 14:25:59 +01:00
# Profiles tab
self.profiles_frame = ttk.Frame(self.notebook)
self.notebook.add(self.profiles_frame, text="Profiles")
2025-09-23 13:59:06 +01:00
# Generation tab
self.generation_frame = ttk.Frame(self.notebook)
self.notebook.add(self.generation_frame, text="Generation")
# Preview tab
self.preview_frame = ttk.Frame(self.notebook)
self.notebook.add(self.preview_frame, text="Preview")
# Setup each tab
self.setup_config_tab()
2025-09-23 14:25:59 +01:00
self.setup_profiles_tab()
2025-09-23 13:59:06 +01:00
self.setup_generation_tab()
self.setup_preview_tab()
# Status bar
self.status_var = tk.StringVar()
self.status_var.set("Ready")
self.status_bar = ttk.Label(self.root, textvariable=self.status_var, relief=tk.SUNKEN)
self.status_bar.pack(side=tk.BOTTOM, fill=tk.X)
# Progress bar
self.progress_var = tk.DoubleVar()
self.progress_bar = ttk.Progressbar(self.root, variable=self.progress_var, maximum=100)
self.progress_bar.pack(side=tk.BOTTOM, fill=tk.X)
def setup_config_tab(self):
"""Set up the configuration tab"""
# Model selection
model_frame = ttk.LabelFrame(self.config_frame, text="AI Model", padding=10)
model_frame.pack(fill=tk.X, padx=10, pady=5)
ttk.Label(model_frame, text="Model:").grid(row=0, column=0, sticky=tk.W, pady=5)
self.model_var = tk.StringVar()
model_combo = ttk.Combobox(model_frame, textvariable=self.model_var, width=50)
model_combo['values'] = [
'SG161222/Realistic_Vision_V6.0_B1',
'digiplay/AbsoluteReality',
'stabilityai/stable-diffusion-2-1',
'runwayml/stable-diffusion-v1-5'
]
model_combo.grid(row=0, column=1, sticky=tk.W, padx=5, pady=5)
model_combo.set('SG161222/Realistic_Vision_V6.0_B1')
# Model directory
ttk.Label(model_frame, text="Model Directory:").grid(row=1, column=0, sticky=tk.W, pady=5)
self.model_dir_var = tk.StringVar()
model_dir_entry = ttk.Entry(model_frame, textvariable=self.model_dir_var, width=50)
model_dir_entry.grid(row=1, column=1, sticky=tk.W, padx=5, pady=5)
ttk.Button(model_frame, text="Browse...", command=self.browse_model_dir).grid(row=1, column=2, padx=5, pady=5)
# FM Configuration
fm_frame = ttk.LabelFrame(self.config_frame, text="Football Manager", padding=10)
fm_frame.pack(fill=tk.X, padx=10, pady=5)
ttk.Label(fm_frame, text="FM Version:").grid(row=0, column=0, sticky=tk.W, pady=5)
self.fm_version_var = tk.StringVar(value="2024")
fm_version_entry = ttk.Entry(fm_frame, textvariable=self.fm_version_var, width=20)
fm_version_entry.grid(row=0, column=1, sticky=tk.W, padx=5, pady=5)
ttk.Label(fm_frame, text="RTF File:").grid(row=1, column=0, sticky=tk.W, pady=5)
self.rtf_file_var = tk.StringVar()
rtf_entry = ttk.Entry(fm_frame, textvariable=self.rtf_file_var, width=50)
rtf_entry.grid(row=1, column=1, sticky=tk.W, padx=5, pady=5)
ttk.Button(fm_frame, text="Browse...", command=self.browse_rtf_file).grid(row=1, column=2, padx=5, pady=5)
ttk.Label(fm_frame, text="Output Directory:").grid(row=2, column=0, sticky=tk.W, pady=5)
self.output_dir_var = tk.StringVar()
output_entry = ttk.Entry(fm_frame, textvariable=self.output_dir_var, width=50)
output_entry.grid(row=2, column=1, sticky=tk.W, padx=5, pady=5)
ttk.Button(fm_frame, text="Browse...", command=self.browse_output_dir).grid(row=2, column=2, padx=5, pady=5)
2025-09-23 14:28:56 +01:00
# System Information
system_frame = ttk.LabelFrame(self.config_frame, text="System Information", padding=10)
system_frame.pack(fill=tk.X, padx=10, pady=5)
# GPU Status
self.gpu_status_var = tk.StringVar()
self.gpu_status_var.set("Checking GPU status...")
ttk.Label(system_frame, text="GPU Status:").grid(row=0, column=0, sticky=tk.W, pady=2)
ttk.Label(system_frame, textvariable=self.gpu_status_var).grid(row=0, column=1, sticky=tk.W, padx=5, pady=2)
# GPU Memory
self.gpu_memory_var = tk.StringVar()
self.gpu_memory_var.set("N/A")
ttk.Label(system_frame, text="GPU Memory:").grid(row=1, column=0, sticky=tk.W, pady=2)
ttk.Label(system_frame, textvariable=self.gpu_memory_var).grid(row=1, column=1, sticky=tk.W, padx=5, pady=2)
# Force CPU option
self.force_cpu_var = tk.StringVar(value="false")
ttk.Checkbutton(system_frame, text="Force CPU usage (disable GPU)", variable=self.force_cpu_var, onvalue="true", offvalue="false", command=self.update_gpu_settings).grid(row=2, column=0, columnspan=2, sticky=tk.W, pady=5)
# System details
ttk.Label(system_frame, text="PyTorch Version:").grid(row=4, column=0, sticky=tk.W, pady=2)
self.pytorch_version_var = tk.StringVar()
self.pytorch_version_var.set("Checking...")
ttk.Label(system_frame, textvariable=self.pytorch_version_var).grid(row=4, column=1, sticky=tk.W, padx=5, pady=2)
ttk.Label(system_frame, text="CUDA Available:").grid(row=5, column=0, sticky=tk.W, pady=2)
self.cuda_available_var = tk.StringVar()
self.cuda_available_var.set("Checking...")
ttk.Label(system_frame, textvariable=self.cuda_available_var).grid(row=5, column=1, sticky=tk.W, padx=5, pady=2)
# Refresh system info button
ttk.Button(system_frame, text="Refresh System Info", command=self.refresh_system_info).grid(row=6, column=0, columnspan=2, pady=5)
2025-09-23 13:59:06 +01:00
# Control buttons
button_frame = ttk.Frame(self.config_frame)
button_frame.pack(fill=tk.X, padx=10, pady=10)
ttk.Button(button_frame, text="Save Configuration", command=self.save_config).pack(side=tk.LEFT, padx=5)
ttk.Button(button_frame, text="Load Configuration", command=self.load_config).pack(side=tk.LEFT, padx=5)
ttk.Button(button_frame, text="Reset to Defaults", command=self.reset_config).pack(side=tk.LEFT, padx=5)
def setup_generation_tab(self):
"""Set up the generation tab"""
# Player selection
player_frame = ttk.LabelFrame(self.generation_frame, text="Player Selection", padding=10)
player_frame.pack(fill=tk.X, padx=10, pady=5)
self.player_mode_var = tk.StringVar(value="all")
ttk.Radiobutton(player_frame, text="Process all players", variable=self.player_mode_var, value="all", command=self.update_player_controls).grid(row=0, column=0, sticky=tk.W, pady=5)
ttk.Radiobutton(player_frame, text="Process specific player", variable=self.player_mode_var, value="specific", command=self.update_player_controls).grid(row=0, column=1, sticky=tk.W, pady=5)
ttk.Label(player_frame, text="Player UID:").grid(row=1, column=0, sticky=tk.W, pady=5)
self.player_uid_var = tk.StringVar()
self.player_uid_entry = ttk.Entry(player_frame, textvariable=self.player_uid_var, width=20, state=tk.DISABLED)
self.player_uid_entry.grid(row=1, column=1, sticky=tk.W, padx=5, pady=5)
# Generation options
gen_frame = ttk.LabelFrame(self.generation_frame, text="Generation Options", padding=10)
gen_frame.pack(fill=tk.X, padx=10, pady=5)
self.update_mode_var = tk.StringVar(value="false")
ttk.Checkbutton(gen_frame, text="Update existing (skip already processed)", variable=self.update_mode_var, onvalue="true", offvalue="false").grid(row=0, column=0, sticky=tk.W, pady=5)
2025-09-23 14:28:56 +01:00
# GPU Memory usage during generation
self.gpu_usage_var = tk.StringVar()
self.gpu_usage_var.set("GPU Memory: N/A")
ttk.Label(gen_frame, textvariable=self.gpu_usage_var).grid(row=1, column=0, sticky=tk.W, pady=5)
2025-09-23 13:59:06 +01:00
# Progress display
progress_frame = ttk.LabelFrame(self.generation_frame, text="Progress", padding=10)
progress_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
# Status text
self.status_text = scrolledtext.ScrolledText(progress_frame, height=10, wrap=tk.WORD)
self.status_text.pack(fill=tk.BOTH, expand=True, pady=5)
# Control buttons
control_frame = ttk.Frame(self.generation_frame)
control_frame.pack(fill=tk.X, padx=10, pady=10)
self.start_button = ttk.Button(control_frame, text="Start Generation", command=self.start_generation)
self.start_button.pack(side=tk.LEFT, padx=5)
self.stop_button = ttk.Button(control_frame, text="Stop Generation", command=self.stop_generation_thread, state=tk.DISABLED)
self.stop_button.pack(side=tk.LEFT, padx=5)
ttk.Button(control_frame, text="Clear Log", command=self.clear_log).pack(side=tk.RIGHT, padx=5)
def setup_preview_tab(self):
"""Set up the preview tab"""
# Image preview
preview_frame = ttk.LabelFrame(self.preview_frame, text="Generated Images", padding=10)
preview_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
# Image display
self.image_label = ttk.Label(preview_frame, text="No images generated yet")
self.image_label.pack(pady=20)
# Image info
info_frame = ttk.Frame(preview_frame)
info_frame.pack(fill=tk.X, pady=10)
ttk.Label(info_frame, text="Current Image:").grid(row=0, column=0, sticky=tk.W)
self.current_image_var = tk.StringVar()
ttk.Label(info_frame, textvariable=self.current_image_var).grid(row=0, column=1, sticky=tk.W, padx=5)
ttk.Label(info_frame, text="Total Generated:").grid(row=1, column=0, sticky=tk.W)
self.total_generated_var = tk.StringVar(value="0")
ttk.Label(info_frame, textvariable=self.total_generated_var).grid(row=1, column=1, sticky=tk.W, padx=5)
2025-09-23 15:33:01 +01:00
# Prompt display
prompt_frame = ttk.LabelFrame(preview_frame, text="Prompt Used", padding=10)
prompt_frame.pack(fill=tk.X, padx=10, pady=5)
self.prompt_text = scrolledtext.ScrolledText(prompt_frame, height=4, wrap=tk.WORD)
self.prompt_text.pack(fill=tk.X, pady=5)
self.prompt_text.config(state=tk.DISABLED) # Make it read-only
2025-09-23 13:59:06 +01:00
# Navigation buttons
nav_frame = ttk.Frame(preview_frame)
nav_frame.pack(fill=tk.X, pady=10)
ttk.Button(nav_frame, text="Previous", command=self.show_previous_image).pack(side=tk.LEFT, padx=5)
ttk.Button(nav_frame, text="Next", command=self.show_next_image).pack(side=tk.LEFT, padx=5)
ttk.Button(nav_frame, text="Refresh", command=self.refresh_preview).pack(side=tk.RIGHT, padx=5)
2025-09-23 14:52:52 +01:00
# Scan for existing images on startup
self.root.after(1000, self.scan_existing_images) # Delay to allow config to load
2025-09-23 13:59:06 +01:00
def browse_model_dir(self):
"""Browse for model directory"""
directory = filedialog.askdirectory(title="Select Model Directory")
if directory:
self.model_dir_var.set(directory)
def browse_rtf_file(self):
"""Browse for RTF file"""
file_path = filedialog.askopenfilename(
title="Select RTF File",
filetypes=[("RTF files", "*.rtf"), ("All files", "*.*")]
)
if file_path:
self.rtf_file_var.set(file_path)
def browse_output_dir(self):
"""Browse for output directory"""
directory = filedialog.askdirectory(title="Select Output Directory")
if directory:
self.output_dir_var.set(directory)
def update_player_controls(self):
"""Update player selection controls based on mode"""
if self.player_mode_var.get() == "specific":
self.player_uid_entry.config(state=tk.NORMAL)
else:
self.player_uid_entry.config(state=tk.DISABLED)
self.player_uid_var.set("")
def save_config(self):
"""Save configuration to file"""
try:
# Update config object
if not self.config:
self.config = configparser.ConfigParser()
self.config.add_section('models')
2025-09-23 14:25:59 +01:00
# Use current profile or create NewGens if none selected
profile_name = self.current_profile or "NewGens"
profile_section = f'profile:{profile_name}'
if not self.config.has_section(profile_section):
self.config.add_section(profile_section)
2025-09-23 13:59:06 +01:00
self.config.set('models', 'model_name', self.model_var.get())
if self.model_dir_var.get():
self.config.set('models', 'model_dir', self.model_dir_var.get())
2025-09-23 14:25:59 +01:00
self.config.set(profile_section, 'football_manager_version', self.fm_version_var.get())
self.config.set(profile_section, 'rtf_file', self.rtf_file_var.get())
self.config.set(profile_section, 'output_dir', self.output_dir_var.get())
2025-09-23 13:59:06 +01:00
# Save to file
with open('user_config.cfg', 'w') as configfile:
self.config.write(configfile)
2025-09-23 14:25:59 +01:00
messagebox.showinfo("Success", f"Configuration saved to profile '{profile_name}'!")
self.log_message(f"Configuration saved to profile '{profile_name}'")
2025-09-23 13:59:06 +01:00
except Exception as e:
messagebox.showerror("Error", f"Failed to save configuration: {str(e)}")
self.log_message(f"Error saving configuration: {str(e)}")
def load_config(self):
"""Load configuration from file"""
try:
if os.path.exists('user_config.cfg'):
self.config = configparser.ConfigParser()
self.config.read('user_config.cfg')
# Load model settings
if 'models' in self.config:
self.model_var.set(self.config.get('models', 'model_name', fallback='SG161222/Realistic_Vision_V6.0_B1'))
self.model_dir_var.set(self.config.get('models', 'model_dir', fallback=''))
2025-09-23 14:25:59 +01:00
# Load current profile settings
if self.current_profile:
profile_section = f'profile:{self.current_profile}'
if profile_section in self.config:
self.fm_version_var.set(self.config.get(profile_section, 'football_manager_version', fallback='2024'))
self.rtf_file_var.set(self.config.get(profile_section, 'rtf_file', fallback=''))
self.output_dir_var.set(self.config.get(profile_section, 'output_dir', fallback=''))
self.log_message(f"Configuration loaded for profile '{self.current_profile}'")
else:
# Profile doesn't exist, create it with defaults
self.create_profile(self.current_profile, '2024', '', './NewGens/')
self.fm_version_var.set('2024')
self.rtf_file_var.set('')
self.output_dir_var.set('./NewGens/')
self.log_message(f"Created new profile '{self.current_profile}' with defaults")
else:
# No profile selected, use defaults
self.fm_version_var.set('2024')
self.rtf_file_var.set('')
self.output_dir_var.set('./NewGens/')
self.log_message("No profile selected, using defaults")
2025-09-23 14:52:52 +01:00
# Scan for existing images after loading config
self.root.after(500, self.scan_existing_images)
2025-09-23 13:59:06 +01:00
else:
self.log_message("No configuration file found, using defaults")
except Exception as e:
messagebox.showerror("Error", f"Failed to load configuration: {str(e)}")
self.log_message(f"Error loading configuration: {str(e)}")
def reset_config(self):
"""Reset configuration to defaults"""
self.model_var.set('SG161222/Realistic_Vision_V6.0_B1')
self.model_dir_var.set('')
self.fm_version_var.set('2024')
self.rtf_file_var.set('')
2025-09-23 14:25:59 +01:00
self.output_dir_var.set('./NewGens/')
# Update current profile with defaults
if self.current_profile:
profile_section = f'profile:{self.current_profile}'
if self.config and self.config.has_section(profile_section):
self.config.set(profile_section, 'football_manager_version', '2024')
self.config.set(profile_section, 'rtf_file', '')
self.config.set(profile_section, 'output_dir', './NewGens/')
with open('user_config.cfg', 'w') as configfile:
self.config.write(configfile)
2025-09-23 13:59:06 +01:00
self.log_message("Configuration reset to defaults")
def log_message(self, message):
"""Add message to log"""
self.status_text.insert(tk.END, f"[{time.strftime('%H:%M:%S')}] {message}\n")
self.status_text.see(tk.END)
def clear_log(self):
"""Clear the log"""
self.status_text.delete(1.0, tk.END)
def start_generation(self):
"""Start the generation process"""
if not self.validate_config():
return
2025-09-23 14:25:59 +01:00
# Set environment variables for the main script
os.environ['FM_NEWGEN_UPDATE_MODE'] = 'true' if self.update_mode_var.get() == "true" else 'false'
os.environ['FM_NEWGEN_PROCESS_PLAYER'] = 'true' if self.player_mode_var.get() == "specific" else 'false'
if self.player_mode_var.get() == "specific":
os.environ['FM_NEWGEN_PLAYER_UID'] = self.player_uid_var.get()
2025-09-23 13:59:06 +01:00
# Disable start button, enable stop button
self.start_button.config(state=tk.DISABLED)
self.stop_button.config(state=tk.NORMAL)
# Reset progress
self.progress_var.set(0)
self.stop_generation = False
# Start generation thread
self.generation_thread = threading.Thread(target=self.generation_worker)
self.generation_thread.daemon = True
self.generation_thread.start()
2025-09-23 14:28:56 +01:00
# Start GPU memory monitoring
self.update_gpu_memory_usage()
2025-09-23 13:59:06 +01:00
def stop_generation_thread(self):
"""Stop the generation process"""
self.stop_generation = True
self.stop_button.config(state=tk.DISABLED)
2025-09-23 14:28:56 +01:00
self.gpu_usage_var.set("GPU Memory: N/A") # Reset GPU usage display
2025-09-23 13:59:06 +01:00
self.log_message("Stopping generation...")
def generation_worker(self):
"""Worker thread for generation process"""
try:
self.log_message("Starting generation process...")
# Load configurations
with open("app_config.json", "r") as f:
app_config = json.load(f)
# Parse RTF file
rtf_file = self.rtf.parse_rtf(self.rtf_file_var.get())
self.log_message(f"Parsed RTF file successfully. Found {len(rtf_file)} players.")
# Determine players to process
update = self.update_mode_var.get() == "true"
process_player = self.player_mode_var.get() == "specific"
if process_player:
player_uuid = int(self.player_uid_var.get())
players_to_process = [p for p in rtf_file if int(p[0]) == player_uuid]
elif update:
# Filter out already processed players
try:
2025-09-23 14:25:59 +01:00
output_dir = self.output_dir_var.get() if self.output_dir_var.get() else "./NewGens/"
values_from_config = extract_from_values(f"{output_dir}/config.xml")
2025-09-23 13:59:06 +01:00
ids_in_b = [item for item in values_from_config]
players_to_process = [item for item in rtf_file if item[0] not in ids_in_b]
except FileNotFoundError:
players_to_process = rtf_file
else:
players_to_process = rtf_file
if len(players_to_process) == 0:
self.log_message("No players to process")
return
self.log_message(f"Processing {len(players_to_process)} players")
# Generate prompts
prompts = generate_prompts_for_players(players_to_process, app_config)
# Generate images
total_players = len(prompts)
for i, prompt in enumerate(prompts):
if self.stop_generation:
break
uid = prompt.split(":")[0]
comfy_prompt = prompt.split(":")[1]
self.log_message(f"Generating image for player {uid} ({i+1}/{total_players})")
generate_image(uid, comfy_prompt)
# Update progress
progress = (i + 1) / total_players * 100
self.progress_var.set(progress)
# Queue image for preview
self.image_queue.put(uid)
# Post-processing
if not self.stop_generation:
try:
2025-09-23 14:10:16 +01:00
# Remove background from images if available
if BG_REMOVAL_AVAILABLE:
try:
2025-09-23 15:15:29 +01:00
output_dir = self.output_dir_var.get() if self.output_dir_var.get() else "./NewGens/"
player_ids = [item[0] for item in players_to_process]
self.log_message(f"Starting background removal for {len(player_ids)} images...")
# Check if images exist before processing
missing_images = []
for uid in player_ids:
image_path = os.path.join(output_dir, f"{uid}.png")
if not os.path.exists(image_path):
missing_images.append(uid)
if missing_images:
self.log_message(f"Warning: {len(missing_images)} images not found: {missing_images[:5]}...")
# Only process existing images
existing_ids = [uid for uid in player_ids if os.path.exists(os.path.join(output_dir, f"{uid}.png"))]
if existing_ids:
self.log_message(f"Processing {len(existing_ids)} images for background removal...")
processed_count = remove_bg_from_file_list(output_dir, existing_ids, use_gpu=False)
self.log_message(f"Background removal completed: {processed_count}/{len(existing_ids)} images processed successfully.")
else:
self.log_message("No images found for background removal.")
2025-09-23 14:10:16 +01:00
except Exception as e:
2025-09-23 15:15:29 +01:00
self.log_message(f"Background removal failed: {str(e)}")
import traceback
self.log_message(f"Error details: {traceback.format_exc()}")
2025-09-23 14:10:16 +01:00
else:
self.log_message("Background removal not available (rembg not installed). Images will have original backgrounds.")
# Update or create configuration XML
2025-09-23 13:59:06 +01:00
if update:
append_to_config_xml(
self.output_dir_var.get(),
[item[0] for item in players_to_process],
self.fm_version_var.get()
)
else:
create_config_xml(
self.output_dir_var.get(),
[item[0] for item in players_to_process],
self.fm_version_var.get()
)
self.log_message("Configuration XML updated")
except Exception as e:
self.log_message(f"Post-processing failed: {e}")
self.log_message("Generation complete!")
except Exception as e:
self.log_message(f"Generation failed: {str(e)}")
import traceback
self.log_message(traceback.format_exc())
finally:
# Re-enable start button
self.start_button.config(state=tk.NORMAL)
self.stop_button.config(state=tk.DISABLED)
def validate_config(self):
"""Validate configuration before starting"""
if not self.rtf_file_var.get():
messagebox.showerror("Error", "Please select an RTF file")
return False
if not self.output_dir_var.get():
messagebox.showerror("Error", "Please select an output directory")
return False
if not os.path.exists(self.rtf_file_var.get()):
messagebox.showerror("Error", "RTF file does not exist")
return False
return True
def update_image_preview(self):
"""Update image preview periodically"""
try:
while not self.image_queue.empty():
uid = self.image_queue.get()
self.show_image(uid)
except:
pass
# Schedule next update
self.root.after(1000, self.update_image_preview)
def show_image(self, uid):
"""Show an image in the preview"""
try:
image_path = f"{self.output_dir_var.get()}/{uid}.png"
if os.path.exists(image_path):
2025-09-23 14:52:52 +01:00
# Add to generated images list if not already present
if uid not in self.generated_images:
self.generated_images.append(uid)
self.update_total_count()
# Find the index of current image
if uid in self.generated_images:
self.current_image_index = self.generated_images.index(uid)
2025-09-23 13:59:06 +01:00
# Load and resize image
image = Image.open(image_path)
image.thumbnail((300, 300), Image.Resampling.LANCZOS)
# Convert to PhotoImage
photo = ImageTk.PhotoImage(image)
# Update display
self.image_label.config(image=photo, text="")
self.image_label.image = photo # Keep reference
self.current_image_var.set(f"Player {uid}")
2025-09-23 15:33:01 +01:00
# Load and display the prompt for this image
try:
prompt = get_prompt_for_image(uid)
self.prompt_text.config(state=tk.NORMAL)
self.prompt_text.delete(1.0, tk.END)
self.prompt_text.insert(tk.END, prompt)
self.prompt_text.config(state=tk.DISABLED)
except Exception as e:
self.prompt_text.config(state=tk.NORMAL)
self.prompt_text.delete(1.0, tk.END)
self.prompt_text.insert(tk.END, f"Error loading prompt: {str(e)}")
self.prompt_text.config(state=tk.DISABLED)
2025-09-23 13:59:06 +01:00
self.log_message(f"Preview updated: {uid}.png")
except Exception as e:
self.log_message(f"Failed to load image {uid}: {str(e)}")
def show_previous_image(self):
"""Show previous image"""
2025-09-23 14:52:52 +01:00
if not self.generated_images:
self.log_message("No images available to navigate")
return
if self.current_image_index <= 0:
self.log_message("Already at first image")
return
self.current_image_index -= 1
uid = self.generated_images[self.current_image_index]
self.show_image(uid)
2025-09-23 13:59:06 +01:00
def show_next_image(self):
"""Show next image"""
2025-09-23 14:52:52 +01:00
if not self.generated_images:
self.log_message("No images available to navigate")
return
if self.current_image_index >= len(self.generated_images) - 1:
self.log_message("Already at last image")
return
self.current_image_index += 1
uid = self.generated_images[self.current_image_index]
self.show_image(uid)
def update_total_count(self):
"""Update the total generated count display"""
count = len(self.generated_images)
self.total_generated_var.set(str(count))
self.log_message(f"Total generated images: {count}")
def scan_existing_images(self):
"""Scan the output directory for existing images"""
try:
output_dir = self.output_dir_var.get()
if not output_dir or not os.path.exists(output_dir):
return
# Find all PNG files in the output directory
existing_images = []
for file in os.listdir(output_dir):
if file.endswith('.png') and file[:-4].isdigit():
uid = file[:-4] # Remove .png extension
existing_images.append(uid)
# Sort by UID (numerically)
existing_images.sort(key=int)
# Update our tracking list
self.generated_images = existing_images
self.update_total_count()
if existing_images:
self.log_message(f"Found {len(existing_images)} existing images")
2025-09-23 15:33:01 +01:00
# Show the first image if available
self.show_image(existing_images[0])
2025-09-23 14:52:52 +01:00
else:
self.log_message("No existing images found")
except Exception as e:
self.log_message(f"Error scanning existing images: {str(e)}")
2025-09-23 13:59:06 +01:00
def refresh_preview(self):
"""Refresh the preview"""
self.log_message("Refreshing preview...")
2025-09-23 14:52:52 +01:00
self.scan_existing_images()
if self.generated_images:
self.show_image(self.generated_images[0]) # Show first image
2025-09-23 13:59:06 +01:00
def main():
"""Main function"""
root = tk.Tk()
app = FMFaceGeneratorGUI(root)
root.mainloop()
if __name__ == "__main__":
main()