import os import sys import json import config import google.generativeai as genai from flask import Flask from rich.console import Console from rich.panel import Panel from rich.prompt import Prompt, IntPrompt, Confirm from rich.table import Table from modules import ai, utils from modules.web_db import db, User, Project console = Console() try: ai.init_models() except Exception as e: console.print(f"[bold red]❌ CRITICAL: AI Model Initialization failed.[/bold red]") console.print(f"[red]Error: {e}[/red]") Prompt.ask("Press Enter to exit...") sys.exit(1) # --- DB SETUP FOR WIZARD --- app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{os.path.join(config.DATA_DIR, "bookapp.db")}' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db.init_app(app) # --- DEFINITIONS --- if not os.path.exists(config.PROJECTS_DIR): os.makedirs(config.PROJECTS_DIR) if not os.path.exists(config.PERSONAS_DIR): os.makedirs(config.PERSONAS_DIR) class BookWizard: def __init__(self): self.project_name = "New_Project" self.project_path = None self.user = None self.data = { "project_metadata": {}, "characters": [], "books": [] } utils.create_default_personas() def _get_or_create_wizard_user(self): # Find or create a default user for CLI operations wizard_user = User.query.filter_by(username="wizard").first() if not wizard_user: console.print("[yellow]Creating default 'wizard' user for CLI operations...[/yellow]") wizard_user = User(username="wizard", password="!", is_admin=True) # Password not used for CLI db.session.add(wizard_user) db.session.commit() return wizard_user def clear(self): os.system('cls' if os.name == 'nt' else 'clear') def ask_gemini_json(self, prompt): text = None try: response = ai.model_logic.generate_content(prompt + "\nReturn ONLY valid JSON.") text = utils.clean_json(response.text) return json.loads(text) except Exception as e: console.print(f"[red]AI Error: {e}[/red]") if text: console.print(f"[dim]Raw output: {text[:150]}...[/dim]") return [] def ask_gemini_text(self, prompt): try: response = ai.model_logic.generate_content(prompt) return response.text.strip() except Exception as e: console.print(f"[red]AI Error: {e}[/red]") return "" def manage_personas(self): while True: self.clear() personas = {} if os.path.exists(config.PERSONAS_FILE): try: with open(config.PERSONAS_FILE, 'r') as f: personas = json.load(f) except: pass console.print(Panel("[bold cyan]🎭 Manage Author Personas[/bold cyan]")) options = list(personas.keys()) for i, name in enumerate(options): console.print(f"[{i+1}] {name}") console.print(f"[{len(options)+1}] Create New Persona") console.print(f"[{len(options)+2}] Back") console.print(f"[{len(options)+3}] Exit") choice = int(Prompt.ask("Select Option", choices=[str(i) for i in range(1, len(options)+4)])) if choice == len(options) + 2: break elif choice == len(options) + 3: sys.exit() selected_key = None details = {} if choice == len(options) + 1: # Create console.print("[yellow]Define New Persona[/yellow]") selected_key = Prompt.ask("Persona Label (e.g. 'Gritty Detective')", default="New Persona") else: # Edit/Delete Menu for specific persona selected_key = options[choice-1] details = personas[selected_key] if isinstance(details, str): details = {"bio": details} console.print(f"\n[bold]Selected: {selected_key}[/bold]") console.print("1. Edit") console.print("2. Delete") console.print("3. Cancel") sub = int(Prompt.ask("Action", choices=["1", "2", "3"], default="1")) if sub == 2: if Confirm.ask(f"Delete '{selected_key}'?", default=False): del personas[selected_key] with open(config.PERSONAS_FILE, 'w') as f: json.dump(personas, f, indent=2) continue elif sub == 3: continue # Edit Fields details['name'] = Prompt.ask("Author Name/Pseudonym", default=details.get('name', "AI Author")) details['age'] = Prompt.ask("Age", default=details.get('age', "Unknown")) details['gender'] = Prompt.ask("Gender", default=details.get('gender', "Unknown")) details['race'] = Prompt.ask("Race/Ethnicity", default=details.get('race', "Unknown")) details['nationality'] = Prompt.ask("Nationality/Country", default=details.get('nationality', "Unknown")) details['language'] = Prompt.ask("Primary Language/Dialect", default=details.get('language', "Standard English")) details['bio'] = Prompt.ask("Writing Style/Bio", default=details.get('bio', "")) # Samples console.print("\n[bold]Style Samples[/bold]") console.print(f"Place text files in the '{config.PERSONAS_DIR}' folder to reference them.") curr_files = details.get('sample_files', []) files_str = ",".join(curr_files) new_files = Prompt.ask("Sample Text Files (comma sep filenames)", default=files_str) details['sample_files'] = [x.strip() for x in new_files.split(',') if x.strip()] details['sample_text'] = Prompt.ask("Manual Sample Paragraph", default=details.get('sample_text', "")) if Confirm.ask("Save Persona?", default=True): personas[selected_key] = details with open(config.PERSONAS_FILE, 'w') as f: json.dump(personas, f, indent=2) def select_mode(self): while True: self.clear() console.print(Panel("[bold blue]🧙‍♂️ BookApp Setup Wizard[/bold blue]")) console.print("1. Create New Project") console.print("2. Open Existing Project") console.print("3. Manage Author Personas") console.print("4. Exit") choice = int(Prompt.ask("Select Mode", choices=["1", "2", "3", "4"], default="1")) if choice == 1: self.user = self._get_or_create_wizard_user() return self.create_new_project() elif choice == 2: self.user = self._get_or_create_wizard_user() if self.open_existing_project(): return True elif choice == 3: # Personas don't need a user context self.manage_personas() else: return False def create_new_project(self): self.clear() console.print(Panel("[bold green]🆕 New Project Setup[/bold green]")) # 1. Ask for Concept first to guide defaults console.print("Tell me about your story idea (or leave empty to start from scratch).") concept = Prompt.ask("Story Concept") suggestions = {} if concept: with console.status("[bold yellow]AI is analyzing your concept...[/bold yellow]"): prompt = f""" Analyze this story concept and suggest metadata for a book or series. CONCEPT: {concept} RETURN JSON with these keys: - title: Suggested book title - genre: Genre - target_audience: e.g. Adult, YA - tone: e.g. Dark, Whimsical - length_category: One of ["00", "0", "01", "1", "2", "2b", "3", "4", "5"] based on likely depth. - estimated_chapters: int (suggested chapter count) - estimated_word_count: string (e.g. "75,000") - include_prologue: boolean - include_epilogue: boolean - tropes: list of strings - pov_style: e.g. First Person - time_period: e.g. Modern - spice: e.g. Standard, Explicit - violence: e.g. None, Graphic - is_series: boolean - series_title: string (if series) - narrative_tense: e.g. Past, Present - language_style: e.g. Standard, Flowery - dialogue_style: e.g. Witty, Formal - page_orientation: Portrait, Landscape, or Square - formatting_rules: list of strings """ suggestions = self.ask_gemini_json(prompt) while True: self.clear() console.print(Panel("[bold green]🤖 AI Suggestions[/bold green]")) grid = Table.grid(padding=(0, 2)) grid.add_column(style="bold cyan") grid.add_column() def get_str(k): v = suggestions.get(k, 'N/A') if isinstance(v, list): return ", ".join(v) return str(v) grid.add_row("Title:", get_str('title')) grid.add_row("Genre:", get_str('genre')) grid.add_row("Audience:", get_str('target_audience')) grid.add_row("Tone:", get_str('tone')) len_cat = suggestions.get('length_category', '4') len_label = config.LENGTH_DEFINITIONS.get(len_cat, {}).get('label', 'Novel') grid.add_row("Length:", len_label) grid.add_row("Est. Chapters:", str(suggestions.get('estimated_chapters', 'N/A'))) grid.add_row("Est. Words:", str(suggestions.get('estimated_word_count', 'N/A'))) grid.add_row("Tropes:", get_str('tropes')) grid.add_row("POV:", get_str('pov_style')) grid.add_row("Time:", get_str('time_period')) grid.add_row("Spice:", get_str('spice')) grid.add_row("Violence:", get_str('violence')) grid.add_row("Series:", "Yes" if suggestions.get('is_series') else "No") console.print(grid) console.print("\n[dim]These will be the defaults for the next step.[/dim]") console.print("\n1. Continue (Manual Step-through)") console.print("2. Refine Suggestions with AI") choice = Prompt.ask("Select Option", choices=["1", "2"], default="1") if choice == "1": break else: instruction = Prompt.ask("Instruction (e.g. 'Make it darker', 'Change genre to Sci-Fi')") with console.status("[bold yellow]Refining suggestions...[/bold yellow]"): refine_prompt = f""" Update these project suggestions based on the user instruction. CURRENT JSON: {json.dumps(suggestions)} INSTRUCTION: {instruction} RETURN ONLY VALID JSON with the same keys. """ new_sugg = self.ask_gemini_json(refine_prompt) if new_sugg: suggestions = new_sugg # 2. Select Type (with AI default) default_type = "2" if suggestions.get('is_series') else "1" console.print("1. Standalone Book") console.print("2. Series") choice = int(Prompt.ask("Select Type", choices=["1", "2"], default=default_type)) is_series = (choice == 2) self.configure_details(suggestions, concept, is_series) self.enrich_blueprint() self.refine_blueprint("Review & Edit Bible") self.save_bible() return True def open_existing_project(self): # Query projects from the database for the wizard user projects = Project.query.filter_by(user_id=self.user.id).order_by(Project.name).all() if not projects: console.print(f"[red]No projects found for user '{self.user.username}'. Create one first.[/red]") Prompt.ask("Press Enter to continue...") return False console.print("\n[bold cyan]📂 Select Project[/bold cyan]") for i, p in enumerate(projects): console.print(f"[{i+1}] {p.name}") console.print(f"[{len(projects)+1}] Back") console.print(f"[{len(projects)+2}] Exit") choice = int(Prompt.ask("Select Project", choices=[str(i) for i in range(1, len(projects)+3)])) if choice == len(projects) + 1: return False if choice == len(projects) + 2: sys.exit() selected_project = projects[choice-1] self.project_name = selected_project.name self.project_path = selected_project.folder_path return True def load_bible(self): if not self.project_path: console.print("[red]No project loaded.[/red]") return False path = os.path.join(self.project_path, "bible.json") if os.path.exists(path): with open(path, 'r') as f: self.data = json.load(f) return True console.print("[red]Bible not found.[/red]") return False def configure_details(self, suggestions=None, concept="", is_series=False): if suggestions is None: suggestions = {} console.print("\n[bold blue]📝 Project Details[/bold blue]") # Simplified Persona Selection (Skip creation) personas = {} if os.path.exists(config.PERSONAS_FILE): try: with open(config.PERSONAS_FILE, 'r') as f: personas = json.load(f) except: pass author_details = {} if personas: console.print("\n[bold]Select Author Persona[/bold]") opts = list(personas.keys()) for i, p in enumerate(opts): console.print(f"[{i+1}] {p}") console.print(f"[{len(opts)+1}] None (AI Default)") sel = IntPrompt.ask("Option", choices=[str(i) for i in range(1, len(opts)+2)], default=len(opts)+1) if sel <= len(opts): author_details = personas[opts[sel-1]] if isinstance(author_details, str): author_details = {"bio": author_details} default_author = author_details.get('name', "AI Co-Pilot") author = Prompt.ask("Author Name", default=default_author) genre = Prompt.ask("Genre", default=suggestions.get('genre', "Fiction")) # LENGTH SELECTION table = Table(title="Target Length Options") table.add_column("#"); table.add_column("Type"); table.add_column("Est. Words"); table.add_column("Chapters") for k, v in config.LENGTH_DEFINITIONS.items(): table.add_row(k, v['label'], v['words'], str(v['chapters'])) console.print(table) def_len = suggestions.get('length_category', "4") if def_len not in config.LENGTH_DEFINITIONS: def_len = "4" len_choice = Prompt.ask("Select Target Length", choices=list(config.LENGTH_DEFINITIONS.keys()), default=def_len) # Create a copy so we don't modify the global definition settings = config.LENGTH_DEFINITIONS[len_choice].copy() # AI Defaults def_chapters = suggestions.get('estimated_chapters', settings['chapters']) def_words = suggestions.get('estimated_word_count', settings['words']) def_prologue = suggestions.get('include_prologue', False) def_epilogue = suggestions.get('include_epilogue', False) settings['chapters'] = IntPrompt.ask("Target Chapters", default=int(def_chapters)) settings['words'] = Prompt.ask("Target Word Count", default=str(def_words)) settings['include_prologue'] = Confirm.ask("Include Prologue?", default=def_prologue) settings['include_epilogue'] = Confirm.ask("Include Epilogue?", default=def_epilogue) # --- GENRE STANDARD CHECK --- # Parse current word count selection w_str = str(settings.get('words', '0')).replace(',', '').replace('+', '').lower() avg_words = 0 if '-' in w_str: parts = w_str.split('-') try: avg_words = (int(parts[0].strip()) + int(parts[1].strip().replace('k','000'))) // 2 except: pass else: try: avg_words = int(w_str.replace('k', '000')) except: pass # Define rough standards std_target = 0 g_lower = genre.lower() if "fantasy" in g_lower or "sci-fi" in g_lower or "space" in g_lower or "epic" in g_lower: std_target = 100000 elif "thriller" in g_lower or "horror" in g_lower or "historical" in g_lower: std_target = 85000 elif "romance" in g_lower or "mystery" in g_lower: std_target = 70000 elif "young adult" in g_lower or "ya" in g_lower: std_target = 60000 if std_target > 0 and avg_words > 0: # If difference is > 25%, warn user if abs(std_target - avg_words) / std_target > 0.25: console.print(f"\n[bold yellow]⚠️ Genre Advisory:[/bold yellow] Standard length for {genre} is approx {std_target:,} words.") if Confirm.ask(f"Update target to {std_target:,} words?", default=True): settings['words'] = f"{std_target:,}" # TROPES console.print("\n[italic]Note: Tropes are recurring themes (e.g. 'Chosen One').[/italic]") def_tropes = ", ".join(suggestions.get('tropes', [])) tropes_input = Prompt.ask("Tropes/Themes (comma sep)", default=def_tropes) sel_tropes = [x.strip() for x in tropes_input.split(',')] if tropes_input else [] # TITLE # If series, this is Series Title. If book, Book Title. title = Prompt.ask("Book Title (Leave empty for AI)", default=suggestions.get('title', "")) # PROJECT NAME default_proj = utils.sanitize_filename(title) if title else "New_Project" self.project_name = Prompt.ask("Project Name (Folder)", default=default_proj) # Create Project in DB and set path user_dir = os.path.join(config.DATA_DIR, "users", str(self.user.id)) if not os.path.exists(user_dir): os.makedirs(user_dir) self.project_path = os.path.join(user_dir, self.project_name) if os.path.exists(self.project_path): console.print(f"[yellow]Warning: Project folder '{self.project_path}' already exists.[/yellow]") new_proj = Project(user_id=self.user.id, name=self.project_name, folder_path=self.project_path) db.session.add(new_proj) db.session.commit() console.print(f"[green]Project '{self.project_name}' created in database.[/green]") console.print("\n[italic]Note: Tone describes the overall mood or atmosphere (e.g. Dark, Whimsical, Cynical, Hopeful).[/italic]") tone = Prompt.ask("Tone", default=suggestions.get('tone', "Balanced")) # POV SETTINGS pov_style = Prompt.ask("POV Style (e.g. 'Third Person Limited', 'First Person')", default=suggestions.get('pov_style', "Third Person Limited")) pov_chars_input = Prompt.ask("POV Characters (comma sep, leave empty if single protagonist)", default="") pov_chars = [x.strip() for x in pov_chars_input.split(',')] if pov_chars_input else [] # ADVANCED STYLE tense = Prompt.ask("Narrative Tense (e.g. 'Past', 'Present')", default=suggestions.get('narrative_tense', "Past")) console.print("\n[bold]Content Guidelines[/bold]") spice = Prompt.ask("Spice/Romance (e.g. 'Clean', 'Fade-to-Black', 'Explicit')", default=suggestions.get('spice', "Standard")) violence = Prompt.ask("Violence (e.g. 'None', 'Mild', 'Graphic')", default=suggestions.get('violence', "Standard")) language = Prompt.ask("Language (e.g. 'No Swearing', 'Mild', 'Heavy')", default=suggestions.get('language_style', "Standard")) dialogue_style = Prompt.ask("Dialogue Style (e.g. 'Witty', 'Formal', 'Slang-heavy')", default=suggestions.get('dialogue_style', "Standard")) console.print("\n[bold]Formatting & World Rules[/bold]") time_period = Prompt.ask("Time Period/Tech (e.g. 'Modern', '1990s', 'No Cellphones')", default=suggestions.get('time_period', "Modern")) # Visuals orientation = Prompt.ask("Page Orientation", choices=["Portrait", "Landscape", "Square"], default=suggestions.get('page_orientation', "Portrait")) console.print("[italic]Define formatting rules (e.g. 'Chapter Headers: POV + Title', 'Text Messages: Italic').[/italic]") def_fmt = ", ".join(suggestions.get('formatting_rules', ["Chapter Headers: Number + Title", "Text Messages: Italic", "Thoughts: Italic"])) fmt_input = Prompt.ask("Formatting Rules (comma sep)", default=def_fmt) fmt_rules = [x.strip() for x in fmt_input.split(',')] if fmt_input else [] # Update book_metadata with new fields style_data = { "tone": tone, "tropes": sel_tropes, "pov_style": pov_style, "pov_characters": pov_chars, "tense": tense, "spice": spice, "violence": violence, "language": language, "dialogue_style": dialogue_style, "time_period": time_period, "page_orientation": orientation, "formatting_rules": fmt_rules } self.data['project_metadata'] = { "title": title, "author": author, "author_details": author_details, "author_bio": author_details.get('bio', ''), "genre": genre, "target_audience": Prompt.ask("Audience", default=suggestions.get('target_audience', "Adult")), "is_series": is_series, "length_settings": settings, "style": style_data } # Initialize Books List self.data['books'] = [] if is_series: count = IntPrompt.ask("How many books in the series?", default=3) for i in range(count): self.data['books'].append({ "book_number": i+1, "title": f"Book {i+1}", "manual_instruction": concept if i==0 else "", "plot_beats": [] }) else: self.data['books'].append({ "book_number": 1, "title": title, "manual_instruction": concept, "plot_beats": [] }) def enrich_blueprint(self): console.print("\n[bold yellow]✨ Generating full Book Bible (Characters, Plot, etc.)...[/bold yellow]") prompt = f""" You are a Creative Director. Create a comprehensive Book Bible for the following project. PROJECT METADATA: {json.dumps(self.data['project_metadata'])} EXISTING BOOKS STRUCTURE: {json.dumps(self.data['books'])} TASK: 1. Create a list of Main Characters (Global for the project). 2. For EACH book in the 'books' list: - Generate a catchy Title (if not provided). - Write a 'manual_instruction' (Plot Summary). - Generate 'plot_beats' (10-15 chronological beats). RETURN JSON in standard Bible format: {{ "characters": [ {{ "name": "...", "role": "...", "description": "..." }} ], "books": [ {{ "book_number": 1, "title": "...", "manual_instruction": "...", "plot_beats": ["...", "..."] }}, ... ] }} """ new_data = self.ask_gemini_json(prompt) if new_data: if 'characters' in new_data: self.data['characters'] = new_data['characters'] # Filter defaults self.data['characters'] = [c for c in self.data['characters'] if c.get('name') and c.get('name').lower() not in ['name', 'character name', 'role', 'protagonist', 'unknown']] if 'books' in new_data: # Merge book data carefully ai_books = {b.get('book_number'): b for b in new_data['books']} for i, book in enumerate(self.data['books']): b_num = book.get('book_number', i+1) if b_num in ai_books: src = ai_books[b_num] book['title'] = src.get('title', book['title']) book['manual_instruction'] = src.get('manual_instruction', book['manual_instruction']) book['plot_beats'] = src.get('plot_beats', []) console.print("[green]Blueprint enriched (Missing data filled)![/green]") def display_summary(self, data): meta = data.get('project_metadata', {}) length = meta.get('length_settings', {}) style = meta.get('style', {}) # Metadata Grid grid = Table.grid(padding=(0, 2)) grid.add_column(style="bold cyan") grid.add_column() grid.add_row("Title:", meta.get('title', 'N/A')) grid.add_row("Author:", meta.get('author', 'N/A')) grid.add_row("Genre:", meta.get('genre', 'N/A')) grid.add_row("Audience:", meta.get('target_audience', 'N/A')) # Dynamic Style Display # Define explicit order for common fields ordered_keys = [ "tone", "pov_style", "pov_characters", "tense", "spice", "violence", "language", "dialogue_style", "time_period", "page_orientation", "tropes" ] defaults = { "tone": "Balanced", "pov_style": "Third Person Limited", "tense": "Past", "spice": "Standard", "violence": "Standard", "language": "Standard", "dialogue_style": "Standard", "time_period": "Modern", "page_orientation": "Portrait" } # 1. Show ordered keys first for k in ordered_keys: val = style.get(k) if val in [None, "", "N/A"]: val = defaults.get(k, 'N/A') if isinstance(val, list): val = ", ".join(val) if isinstance(val, bool): val = "Yes" if val else "No" grid.add_row(f"{k.replace('_', ' ').title()}:", str(val)) # 2. Show remaining keys for k, v in style.items(): if k not in ordered_keys and k != 'formatting_rules': val = ", ".join(v) if isinstance(v, list) else str(v) grid.add_row(f"{k.replace('_', ' ').title()}:", val) len_str = f"{length.get('label', 'N/A')} ({length.get('words', 'N/A')} words, {length.get('chapters', 'N/A')} ch)" extras = [] if length.get('include_prologue'): extras.append("Prologue") if length.get('include_epilogue'): extras.append("Epilogue") if extras: len_str += f" + {', '.join(extras)}" grid.add_row("Length:", len_str) grid.add_row("Series:", "Yes" if meta.get('is_series') else "No") console.print(Panel(grid, title="[bold blue]📖 Project Metadata[/bold blue]", expand=False)) # Formatting Rules Table fmt_rules = style.get('formatting_rules', []) if fmt_rules: fmt_table = Table(title="Formatting Rules", show_header=False, box=None, expand=True) for i, r in enumerate(fmt_rules): fmt_table.add_row(f"[bold]{i+1}.[/bold]", str(r)) console.print(Panel(fmt_table, title="[bold blue]🎨 Formatting[/bold blue]")) # Characters Table char_table = Table(title="👥 Characters", show_header=True, header_style="bold magenta", expand=True) char_table.add_column("Name", style="green") char_table.add_column("Role") char_table.add_column("Description") for c in data.get('characters', []): # Removed truncation to show full description char_table.add_row(c.get('name', '-'), c.get('role', '-'), c.get('description', '-')) console.print(char_table) # Books List for book in data.get('books', []): console.print(f"\n[bold cyan]📘 Book {book.get('book_number')}: {book.get('title')}[/bold cyan]") console.print(f"[italic]{book.get('manual_instruction')}[/italic]") beats = book.get('plot_beats', []) if beats: beat_table = Table(show_header=False, box=None, expand=True) for i, b in enumerate(beats): beat_table.add_row(f"[bold]{i+1}.[/bold]", str(b)) console.print(beat_table) def refine_blueprint(self, title="Refine Blueprint"): while True: self.clear() console.print(Panel(f"[bold blue]🔧 {title}[/bold blue]")) self.display_summary(self.data) console.print("\n[dim](Full JSON loaded)[/dim]") change = Prompt.ask("\n[bold green]Enter instruction to change (e.g. 'Make it darker', 'Rename Bob', 'Add a twist') or 'done'[/bold green]") if change.lower() == 'done': break # Inner loop for refinement current_data = self.data instruction = change while True: with console.status("[bold green]AI is updating blueprint...[/bold green]"): prompt = f""" Act as a Book Editor. CURRENT JSON: {json.dumps(current_data)} USER INSTRUCTION: {instruction} TASK: Update the JSON based on the instruction. Maintain valid JSON structure. RETURN ONLY THE JSON. """ new_data = self.ask_gemini_json(prompt) if not new_data: console.print("[red]AI failed to generate valid JSON.[/red]") break self.clear() console.print(Panel("[bold blue]👀 Review AI Changes[/bold blue]")) self.display_summary(new_data) feedback = Prompt.ask("\n[bold green]Is this good? (Type 'yes' to save, or enter feedback to refine)[/bold green]") if feedback.lower() == 'yes': self.data = new_data console.print("[green]Changes saved![/green]") break else: current_data = new_data instruction = feedback def save_bible(self): if not self.project_path: console.print("[red]Project path not set. Cannot save bible.[/red]") return None if not os.path.exists(self.project_path): os.makedirs(self.project_path) filename = os.path.join(self.project_path, "bible.json") with open(filename, 'w') as f: json.dump(self.data, f, indent=2) console.print(Panel(f"[bold green]✅ Bible saved to: {filename}[/bold green]")) return filename def manage_runs(self): runs_dir = os.path.join(self.project_path, "runs") if not os.path.exists(runs_dir): console.print("[red]No runs found for this project.[/red]") Prompt.ask("Press Enter...") return runs = sorted([d for d in os.listdir(runs_dir) if os.path.isdir(os.path.join(runs_dir, d)) and d.startswith("run_")], key=lambda x: int(x.split('_')[1]) if x.split('_')[1].isdigit() else 0, reverse=True) if not runs: console.print("[red]No runs found.[/red]") Prompt.ask("Press Enter...") return while True: self.clear() console.print(Panel(f"[bold blue]Runs for: {self.project_name}[/bold blue]")) for i, r in enumerate(runs): console.print(f"[{i+1}] {r}") console.print(f"[{len(runs)+1}] Back") console.print(f"[{len(runs)+2}] Exit") choice = int(Prompt.ask("Select Run", choices=[str(i) for i in range(1, len(runs)+3)])) if choice == len(runs) + 1: break elif choice == len(runs) + 2: sys.exit() selected_run = runs[choice-1] run_path = os.path.join(runs_dir, selected_run) self.manage_specific_run(run_path) def manage_specific_run(self, run_path): while True: self.clear() console.print(Panel(f"[bold blue]Run: {os.path.basename(run_path)}[/bold blue]")) # Detect sub-books (Series Run) subdirs = sorted([d for d in os.listdir(run_path) if os.path.isdir(os.path.join(run_path, d)) and d.startswith("Book_")]) if subdirs: console.print("[italic]Series Run Detected[/italic]") for i, s in enumerate(subdirs): console.print(f"[{i+1}] Manage {s}") idx_open = len(subdirs) + 1 idx_back = len(subdirs) + 2 idx_exit = len(subdirs) + 3 console.print(f"[{idx_open}] Open Run Folder") console.print(f"[{idx_back}] Back") console.print(f"[{idx_exit}] Exit") choice = int(Prompt.ask("Select Option", choices=[str(i) for i in range(1, idx_exit+1)])) if choice <= len(subdirs): book_path = os.path.join(run_path, subdirs[choice-1]) self.manage_single_book_folder(book_path) elif choice == idx_open: self.open_folder(run_path) elif choice == idx_back: break elif choice == idx_exit: sys.exit() def manage_single_book_folder(self, folder_path): while True: self.clear() console.print(Panel(f"[bold blue]Manage: {os.path.basename(folder_path)}[/bold blue]")) console.print("1. Regenerate Cover & Recompile EPUB") console.print("2. Open Folder") console.print("3. Back") choice = int(Prompt.ask("Select Action", choices=["1", "2", "3"])) if choice == 1: import main bp_path = os.path.join(folder_path, "final_blueprint.json") ms_path = os.path.join(folder_path, "manuscript.json") if os.path.exists(bp_path) and os.path.exists(ms_path): with console.status("[bold yellow]Regenerating Cover...[/bold yellow]"): with open(bp_path, 'r') as f: bp = json.load(f) with open(ms_path, 'r') as f: ms = json.load(f) # Check/Generate Tracking events_path = os.path.join(folder_path, "tracking_events.json") chars_path = os.path.join(folder_path, "tracking_characters.json") tracking = {"events": [], "characters": {}} if os.path.exists(events_path): tracking['events'] = utils.load_json(events_path) if os.path.exists(chars_path): tracking['characters'] = utils.load_json(chars_path) main.ai.init_models() if not tracking['events'] and not tracking['characters']: # Fallback: Use Blueprint data console.print("[yellow]Tracking missing. Populating from Blueprint...[/yellow]") tracking['events'] = bp.get('plot_beats', []) tracking['characters'] = {} for c in bp.get('characters', []): name = c.get('name', 'Unknown') tracking['characters'][name] = { "descriptors": [c.get('description', '')], "likes_dislikes": [], "last_worn": "Unknown" } with open(events_path, 'w') as f: json.dump(tracking['events'], f, indent=2) with open(chars_path, 'w') as f: json.dump(tracking['characters'], f, indent=2) main.marketing.generate_cover(bp, folder_path, tracking) main.export.compile_files(bp, ms, folder_path) console.print("[green]Cover updated and EPUB recompiled![/green]") Prompt.ask("Press Enter...") else: console.print("[red]Missing blueprint or manuscript in run folder.[/red]") Prompt.ask("Press Enter...") elif choice == 2: self.open_folder(folder_path) elif choice == 3: break def open_folder(self, path): if os.name == 'nt': os.startfile(path) else: os.system(f"open '{path}'") if __name__ == "__main__": w = BookWizard() with app.app_context(): try: if w.select_mode(): while True: w.clear() console.print(Panel(f"[bold blue]📂 Project: {w.project_name}[/bold blue]")) console.print("1. Edit Bible") console.print("2. Run Book Generation") console.print("3. Manage Runs") console.print("4. Exit") choice = int(Prompt.ask("Select Option", choices=["1", "2", "3", "4"])) if choice == 1: if w.load_bible(): w.refine_blueprint("Refine Bible") w.save_bible() elif choice == 2: if w.load_bible(): bible_path = os.path.join(w.project_path, "bible.json") import main main.run_generation(bible_path, interactive=True) Prompt.ask("\nGeneration complete. Press Enter...") elif choice == 3: # Manage runs w.manage_runs() else: break else: pass except KeyboardInterrupt: console.print("\n[red]Cancelled.[/red]")