# filename: prompt_builder.py import os from collections import defaultdict from pathlib import Path import logging import json import configparser # Configure logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[logging.StreamHandler()] ) logger = logging.getLogger("PromptBuilder") # Optional imports with better error handling try: from llama_cpp import Llama LLAMA_AVAILABLE = True except ImportError: logger.warning("llama-cpp-python not installed. LLM enhancement will be disabled.") LLAMA_AVAILABLE = False try: import gradio as gr GRADIO_AVAILABLE = True except ImportError: logger.error("Gradio not installed. Cannot launch UI.") GRADIO_AVAILABLE = False class PromptBuilder: def __init__(self, options_dir="prompt_options", config_file="config.ini"): self.options_dir = options_dir self.config = self._load_config(config_file) self.options = self._load_options() self.llm = None # Initialize LLM if available and configured if LLAMA_AVAILABLE and self.config.get('llm', 'enabled') == 'True': self._initialize_llm() def _load_config(self, config_file): """Load configuration from config.ini file or create with defaults""" config = configparser.ConfigParser() # Default configuration config['general'] = { 'options_dir': 'prompt_options', 'history_file': 'prompt_history.json' } config['llm'] = { 'enabled': 'True', 'model_path': 'models/model.gguf', 'system_prompt': 'You are a visual prompt engineer and expert in visual descriptions influenced by keywords.', 'n_gpu_layers': '30', 'seed': '1337', 'context_size': '2048' } # Try to load existing config if os.path.exists(config_file): try: config.read(config_file) logger.info(f"Loaded configuration from {config_file}") except Exception as e: logger.error(f"Error loading config: {e}") else: # Save default config try: os.makedirs(os.path.dirname(config_file), exist_ok=True) with open(config_file, 'w') as f: config.write(f) logger.info(f"Created default configuration at {config_file}") except Exception as e: logger.error(f"Error creating config: {e}") return config def _initialize_llm(self): """Initialize the LLM with configuration parameters""" try: model_path = self.config.get('llm', 'model_path') if not os.path.exists(model_path): logger.error(f"Model file not found: {model_path}") return self.llm = Llama( model_path=model_path, n_gpu_layers=self.config.getint('llm', 'n_gpu_layers'), seed=self.config.getint('llm', 'seed'), n_ctx=self.config.getint('llm', 'context_size'), ) logger.info(f"LLM initialized successfully with model: {model_path}") except Exception as e: logger.error(f"Failed to initialize LLM: {e}") self.llm = None def _ensure_directory_exists(self, directory): """Ensure the specified directory exists""" Path(directory).mkdir(parents=True, exist_ok=True) def _load_options(self): """Load prompt options from text files""" options = defaultdict(dict) # Ensure options directory exists self._ensure_directory_exists(self.options_dir) try: for filename in os.listdir(self.options_dir): if filename.endswith(".txt"): path = os.path.join(self.options_dir, filename) key = filename.replace(".txt", "") try: if '.' in key: group, field = key.split('.', 1) with open(path, "r", encoding="utf-8") as f: options[group][field] = [line.strip() for line in f if line.strip()] else: # top-level group with open(path, "r", encoding="utf-8") as f: options["general"][key] = [line.strip() for line in f if line.strip()] except Exception as e: logger.error(f"Error loading options from {path}: {e}") except Exception as e: logger.error(f"Error accessing options directory: {e}") return options def get_choices(self, group, field): """Get choices for a specific group and field""" return self.options.get(group, {}).get(field, []) def build_prompt(self, base_prompt="", custom_tags=None, enhance=False, **field_values): """Build a prompt from selected options""" parts = [base_prompt] if base_prompt else [] # Add field values to the prompt parts for key, value in field_values.items(): if not value: continue if key in ("styles", "lighting", "mood"): if key == "styles": parts.append(f"in {value} style") elif key == "lighting": parts.append(f"with {value} lighting") elif key == "mood": parts.append(f"evoking a {value} mood") else: parts.append(value) # Add custom tags if custom_tags: parts.append(custom_tags) # Join parts into a basic prompt basic_prompt = ", ".join(filter(None, parts)) # Enhance the prompt if requested and LLM is available if enhance and self.llm is not None: try: return self.enhance_prompt(basic_prompt) except Exception as e: logger.error(f"Error enhancing prompt: {e}") return f"Error enhancing prompt: {e}\nBasic prompt: {basic_prompt}" else: return basic_prompt return "No prompt built!" def enhance_prompt(self, prompt: str) -> str: """Enhance a prompt using the LLM""" if not self.llm: return f"LLM not available. Basic prompt: {prompt}" try: system_prompt = self.config.get('llm', 'system_prompt') base_prompt = f"""<|begin_of_text|><|start_header_id|>system<|end_header_id|> {system_prompt}<|eot_id|><|start_header_id|>user<|end_header_id|> {prompt}<|eot_id|><|start_header_id|>assistant<|end_header_id|> """ output = self.llm( base_prompt, max_tokens=256, # Generate more tokens for better descriptions stop=["<|eot_id|>"], echo=False, ) if output and "choices" in output and output["choices"]: return output["choices"][0]["text"].strip() else: logger.warning("LLM returned empty or invalid response") return prompt except Exception as e: logger.error(f"Error in LLM inference: {e}") return f"Error enhancing: {e}\nOriginal prompt: {prompt}" def save_prompt_to_history(self, prompt): """Save generated prompt to history file""" history_file = self.config.get('general', 'history_file') try: history = [] if os.path.exists(history_file): with open(history_file, 'r', encoding='utf-8') as f: history = json.load(f) # Add new prompt with timestamp from datetime import datetime history.append({ "prompt": prompt, "timestamp": datetime.now().isoformat() }) # Keep only last 100 prompts history = history[-100:] with open(history_file, 'w', encoding='utf-8') as f: json.dump(history, f, indent=2) logger.info(f"Saved prompt to history: {prompt[:30]}...") except Exception as e: logger.error(f"Error saving prompt to history: {e}") def create_ui(builder): """Create the Gradio UI""" with gr.Blocks(title="Prompt Builder UI") as demo: gr.Markdown("# ๐ŸŒŸ Prompt Builder\nCompose generative prompts using curated options.") with gr.Row(): with gr.Column(scale=3): base_prompt = gr.Textbox( label="Base Prompt", placeholder="e.g., A portrait of...", lines=2 ) with gr.Column(scale=1): llama_prompt_enhancing = gr.Checkbox( label="Enhance with LLM", value=builder.llm is not None, interactive=builder.llm is not None, info="Enhance prompt with LLM" if builder.llm is not None else "LLM not available" ) custom_tags = gr.Textbox( label="Custom Tags", placeholder="e.g., cinematic, trending on ArtStation, detailed, 8k" ) with gr.Tabs(): with gr.TabItem("Character"): with gr.Row(): with gr.Column(): gender = gr.Dropdown( choices=builder.get_choices("character", "gender"), label="Gender" ) body = gr.Dropdown( choices=builder.get_choices("character", "body"), label="Body" ) clothing = gr.Dropdown( choices=builder.get_choices("character", "clothing"), label="Clothing" ) with gr.Column(): hair = gr.Dropdown( choices=builder.get_choices("character", "hair"), label="Hair" ) eyes = gr.Dropdown( choices=builder.get_choices("character", "eyes"), label="Eyes" ) with gr.TabItem("Background"): with gr.Row(): with gr.Column(): land_type = gr.Dropdown( choices=builder.get_choices("background", "land_type"), label="Land Type" ) sky = gr.Dropdown( choices=builder.get_choices("background", "sky"), label="Sky" ) with gr.Column(): flora = gr.Dropdown( choices=builder.get_choices("background", "flora"), label="Flora" ) fauna = gr.Dropdown( choices=builder.get_choices("background", "fauna"), label="Fauna" ) with gr.TabItem("Style"): with gr.Row(): with gr.Column(): styles = gr.Dropdown( choices=builder.get_choices("general", "styles"), label="Style" ) with gr.Column(): lighting = gr.Dropdown( choices=builder.get_choices("general", "lighting"), label="Lighting" ) mood = gr.Dropdown( choices=builder.get_choices("general", "mood"), label="Mood" ) with gr.Row(): with gr.Column(scale=4): output = gr.Textbox( label="Generated Prompt", lines=4 ) with gr.Column(scale=1): copy_btn = gr.Button("๐Ÿ“‹ Copy to Clipboard") save_btn = gr.Button("๐Ÿ’พ Save to History") clear_btn = gr.Button("๐Ÿงน Clear All") with gr.Row(): generate_btn = gr.Button("๐Ÿ”ฎ Build Prompt", variant="primary", size="lg") # Handle events def generate_prompt_handler( base_prompt, custom_tags, gender, body, clothing, hair, eyes, land_type, sky, flora, fauna, styles, lighting, mood, llama_prompt_enhancing ): result = builder.build_prompt( base_prompt=base_prompt, enhance=llama_prompt_enhancing, custom_tags=custom_tags, gender=gender, body=body, clothing=clothing, hair=hair, eyes=eyes, land_type=land_type, sky=sky, flora=flora, fauna=fauna, styles=styles, lighting=lighting, mood=mood ) return result def save_to_history(prompt): if prompt: builder.save_prompt_to_history(prompt) return gr.update(value="Saved to history!") return gr.update(value="Nothing to save") def clear_all(): return "", "", None, None, None, None, None, None, None, None, None, None, None, None, False, "" # Connect event handlers generate_btn.click( fn=generate_prompt_handler, inputs=[ base_prompt, custom_tags, gender, body, clothing, hair, eyes, land_type, sky, flora, fauna, styles, lighting, mood, llama_prompt_enhancing ], outputs=[output] ) save_btn.click( fn=save_to_history, inputs=[output], outputs=[output] ) clear_btn.click( fn=clear_all, inputs=[], outputs=[ base_prompt, custom_tags, gender, body, clothing, hair, eyes, land_type, sky, flora, fauna, styles, lighting, mood, llama_prompt_enhancing, output ] ) # JavaScript for copy to clipboard function copy_btn.click( None, _js=""" () => { const output = document.querySelector('#output textarea'); if (output) { navigator.clipboard.writeText(output.value); return "Copied to clipboard!"; } return "Nothing to copy"; } """, outputs=[output] ) return demo def main(): """Main entry point for the application""" builder = PromptBuilder() if not GRADIO_AVAILABLE: logger.error("Cannot start UI - Gradio not available") return demo = create_ui(builder) demo.launch() if __name__ == "__main__": main()