2025-09-23 13:59:06 +01:00

500 lines
20 KiB
Python

#!/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
from comfy_fm_newgen import generate_image, get_country_name, generate_prompts_for_players
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 []
from lib.generate_xml import create_config_xml, append_to_config_xml
from lib.xml_reader import extract_from_values
from lib.general import choose_profile, create_or_update, process_player_or_file, get_player_input
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)
# 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()
# Set up the GUI
self.setup_gui()
self.load_config()
# Start image preview update loop
self.update_image_preview()
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")
# 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()
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)
# 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)
# 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)
# 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)
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')
self.config.add_section('profile:NewGens')
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())
self.config.set('profile:NewGens', 'football_manager_version', self.fm_version_var.get())
self.config.set('profile:NewGens', 'rtf_file', self.rtf_file_var.get())
self.config.set('profile:NewGens', 'output_dir', self.output_dir_var.get())
# Save to file
with open('user_config.cfg', 'w') as configfile:
self.config.write(configfile)
messagebox.showinfo("Success", "Configuration saved successfully!")
self.log_message("Configuration saved")
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=''))
# Load FM settings
if 'profile:NewGens' in self.config:
self.fm_version_var.set(self.config.get('profile:NewGens', 'football_manager_version', fallback='2024'))
self.rtf_file_var.set(self.config.get('profile:NewGens', 'rtf_file', fallback=''))
self.output_dir_var.set(self.config.get('profile:NewGens', 'output_dir', fallback=''))
self.log_message("Configuration loaded")
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('')
self.output_dir_var.set('')
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
# 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()
def stop_generation_thread(self):
"""Stop the generation process"""
self.stop_generation = True
self.stop_button.config(state=tk.DISABLED)
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:
values_from_config = extract_from_values(f"{self.output_dir_var.get()}/config.xml")
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:
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):
# 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}")
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"""
self.log_message("Previous image functionality not implemented yet")
def show_next_image(self):
"""Show next image"""
self.log_message("Next image functionality not implemented yet")
def refresh_preview(self):
"""Refresh the preview"""
self.log_message("Refreshing preview...")
# This would scan the output directory and show available images
def main():
"""Main function"""
root = tk.Tk()
app = FMFaceGeneratorGUI(root)
root.mainloop()
if __name__ == "__main__":
main()