import json import random from core import utils from ai import models as ai_models from story.bible_tracker import filter_characters def enrich(bp, folder, context=""): utils.log("ENRICHER", "Fleshing out details from description...") if 'book_metadata' not in bp: bp['book_metadata'] = {} if 'characters' not in bp: bp['characters'] = [] if 'plot_beats' not in bp: bp['plot_beats'] = [] prompt = f""" ROLE: Creative Director TASK: Create a comprehensive Book Bible from the user description. INPUT DATA: - USER_DESCRIPTION: "{bp.get('manual_instruction', 'A generic story')}" - CONTEXT (Sequel): {context} STEPS: 1. Generate a catchy Title. 2. Define the Genre and Tone. 3. Determine the Time Period (e.g. "Modern", "1920s", "Sci-Fi Future"). 4. Define Formatting Rules for text messages, thoughts, and chapter headers. 5. Create Protagonist and Antagonist/Love Interest. - Logic: If sequel, reuse context. If new, create. 6. Outline 5-7 core Plot Beats. 7. Define a 'structure_prompt' describing the narrative arc (e.g. "Hero's Journey", "3-Act Structure", "Detective Procedural"). OUTPUT_FORMAT (JSON): {{ "book_metadata": {{ "title": "Book Title", "genre": "Genre", "content_warnings": ["Violence", "Major Character Death"], "structure_prompt": "...", "style": {{ "tone": "Tone", "time_period": "Modern", "formatting_rules": ["Chapter Headers: Number + Title", "Text Messages: Italic", "Thoughts: Italic"] }} }}, "characters": [ {{ "name": "John Doe", "role": "Protagonist", "description": "Description", "key_events": ["Planned injury in Act 2"] }} ], "plot_beats": [ "Beat 1", "Beat 2", "..." ] }} """ try: response = ai_models.model_logic.generate_content(prompt) utils.log_usage(folder, ai_models.model_logic.name, response.usage_metadata) ai_data = json.loads(utils.clean_json(response.text)) if 'book_metadata' not in bp: bp['book_metadata'] = {} if 'title' not in bp['book_metadata']: bp['book_metadata']['title'] = ai_data.get('book_metadata', {}).get('title') if 'structure_prompt' not in bp['book_metadata']: bp['book_metadata']['structure_prompt'] = ai_data.get('book_metadata', {}).get('structure_prompt') if 'content_warnings' not in bp['book_metadata']: bp['book_metadata']['content_warnings'] = ai_data.get('book_metadata', {}).get('content_warnings', []) if 'style' not in bp['book_metadata']: bp['book_metadata']['style'] = {} source_style = ai_data.get('book_metadata', {}).get('style', {}) for k, v in source_style.items(): if k not in bp['book_metadata']['style']: bp['book_metadata']['style'][k] = v if 'characters' not in bp or not bp['characters']: bp['characters'] = ai_data.get('characters', []) if 'characters' in bp: bp['characters'] = filter_characters(bp['characters']) if 'plot_beats' not in bp or not bp['plot_beats']: bp['plot_beats'] = ai_data.get('plot_beats', []) return bp except Exception as e: utils.log("ENRICHER", f"Enrichment failed: {e}") return bp def plan_structure(bp, folder): utils.log("ARCHITECT", "Creating structure...") structure_type = bp.get('book_metadata', {}).get('structure_prompt') if not structure_type: label = bp.get('length_settings', {}).get('label', 'Novel') structures = { "Chapter Book": "Create a simple episodic structure with clear chapter hooks.", "Young Adult": "Create a character-driven arc with high emotional stakes and a clear 'Coming of Age' theme.", "Flash Fiction": "Create a single, impactful scene structure with a twist.", "Short Story": "Create a concise narrative arc (Inciting Incident -> Rising Action -> Climax -> Resolution).", "Novella": "Create a standard 3-Act Structure.", "Novel": "Create a detailed 3-Act Structure with A and B plots.", "Epic": "Create a complex, multi-arc structure (Hero's Journey) with extensive world-building events." } structure_type = structures.get(label, "Create a 3-Act Structure.") beats_context = bp.get('plot_beats', []) target_chapters = bp.get('length_settings', {}).get('chapters', 'flexible') target_words = bp.get('length_settings', {}).get('words', 'flexible') chars_summary = [{"name": c.get("name"), "role": c.get("role")} for c in bp.get('characters', [])] prompt = f""" ROLE: Story Architect TASK: Create a detailed structural event outline for a {target_chapters}-chapter book. BOOK: - TITLE: {bp['book_metadata']['title']} - GENRE: {bp.get('book_metadata', {}).get('genre', 'Fiction')} - TARGET_CHAPTERS: {target_chapters} - TARGET_WORDS: {target_words} - STRUCTURE: {structure_type} CHARACTERS: {json.dumps(chars_summary)} USER_BEATS (must all be preserved and woven into the outline): {json.dumps(beats_context)} REQUIREMENTS: - Produce enough events to fill approximately {target_chapters} chapters. - Each event must serve a narrative purpose (setup, escalation, reversal, climax, resolution). - Distribute events across a beginning, middle, and end — avoid front-loading. - Character arcs must be visible through the events (growth, change, revelation). OUTPUT_FORMAT (JSON): {{ "events": [{{ "description": "String", "purpose": "String" }}] }} """ try: response = ai_models.model_logic.generate_content(prompt) utils.log_usage(folder, ai_models.model_logic.name, response.usage_metadata) return json.loads(utils.clean_json(response.text))['events'] except: return [] def expand(events, pass_num, target_chapters, bp, folder): utils.log("ARCHITECT", f"Expansion pass {pass_num} | Current Beats: {len(events)} | Target Chaps: {target_chapters}") event_ceiling = int(target_chapters * 1.5) if len(events) >= event_ceiling: task = ( f"The outline already has {len(events)} beats for a {target_chapters}-chapter book — do NOT add more events. " f"Instead, enrich each existing beat's description with more specific detail: setting, characters involved, emotional stakes, and how it connects to what follows." ) else: task = ( f"Expand the outline toward {target_chapters} chapters. " f"Current count: {len(events)} beats. " f"Add intermediate events to fill pacing gaps, deepen subplots, and ensure character arcs are visible. " f"Do not overshoot — aim for {target_chapters} to {event_ceiling} total events." ) original_beats = bp.get('plot_beats', []) prompt = f""" ROLE: Story Architect TASK: {task} ORIGINAL_USER_BEATS (must all remain present): {json.dumps(original_beats)} CURRENT_EVENTS: {json.dumps(events)} RULES: 1. PRESERVE all original user beats — do not remove or alter them. 2. New events must serve a clear narrative purpose (tension, character, world, reversal). 3. Avoid repetitive events — each beat must be distinct. 4. Distribute additions evenly — do not front-load the outline. OUTPUT_FORMAT (JSON): {{ "events": [{{"description": "String", "purpose": "String"}}] }} """ try: response = ai_models.model_logic.generate_content(prompt) utils.log_usage(folder, ai_models.model_logic.name, response.usage_metadata) new_events = json.loads(utils.clean_json(response.text))['events'] if len(new_events) > len(events): utils.log("ARCHITECT", f" -> Added {len(new_events) - len(events)} new beats.") elif len(str(new_events)) > len(str(events)) + 20: utils.log("ARCHITECT", f" -> Fleshed out descriptions (Text grew by {len(str(new_events)) - len(str(events))} chars).") else: utils.log("ARCHITECT", " -> No significant changes.") return new_events except Exception as e: utils.log("ARCHITECT", f" -> Pass skipped due to error: {e}") return events def create_chapter_plan(events, bp, folder): utils.log("ARCHITECT", "Finalizing Chapters...") target = bp['length_settings']['chapters'] words = bp['length_settings'].get('words', 'Flexible') include_prologue = bp.get('length_settings', {}).get('include_prologue', False) include_epilogue = bp.get('length_settings', {}).get('include_epilogue', False) structure_instructions = "" if include_prologue: structure_instructions += "- Include a 'Prologue' (chapter_number: 0) to set the scene.\n" if include_epilogue: structure_instructions += "- Include an 'Epilogue' (chapter_number: 'Epilogue') to wrap up.\n" meta = bp.get('book_metadata', {}) style = meta.get('style', {}) pov_chars = style.get('pov_characters', []) pov_instruction = "" if pov_chars: pov_instruction = f"- Assign a 'pov_character' for each chapter from this list: {json.dumps(pov_chars)}." prompt = f""" ROLE: Pacing Specialist TASK: Group the provided events into chapters for a {meta.get('genre', 'Fiction')} {bp['length_settings'].get('label', 'novel')}. GUIDELINES: - AIM for approximately {target} chapters, but the final count may vary ±15% if the story structure demands it. - TARGET_WORDS for the whole book: {words} - Assign pacing to each chapter: Very Fast / Fast / Standard / Slow / Very Slow - estimated_words per chapter should reflect its pacing: Very Fast ≈ 60% of average, Fast ≈ 80%, Standard ≈ 100%, Slow ≈ 125%, Very Slow ≈ 150% - Do NOT force equal word counts. Natural variation makes the book feel alive. {structure_instructions} {pov_instruction} INPUT_EVENTS: {json.dumps(events)} OUTPUT_FORMAT (JSON): [{{"chapter_number": 1, "title": "String", "pov_character": "String", "pacing": "String", "estimated_words": 2000, "beats": ["String"]}}] """ try: response = ai_models.model_logic.generate_content(prompt) utils.log_usage(folder, ai_models.model_logic.name, response.usage_metadata) plan = json.loads(utils.clean_json(response.text)) target_str = str(words).lower().replace(',', '').replace('k', '000').replace('+', '').replace(' ', '') target_val = 0 if '-' in target_str: try: parts = target_str.split('-') target_val = int((int(parts[0]) + int(parts[1])) / 2) except: pass else: try: target_val = int(target_str) except: pass if target_val > 0: variance = random.uniform(0.92, 1.08) target_val = int(target_val * variance) utils.log("ARCHITECT", f"Word target after variance ({variance:.2f}x): {target_val} words.") current_sum = sum(int(c.get('estimated_words', 0)) for c in plan) if current_sum > 0: base_factor = target_val / current_sum pacing_weight = { 'very fast': 0.60, 'fast': 0.80, 'standard': 1.00, 'slow': 1.25, 'very slow': 1.50 } for c in plan: pw = pacing_weight.get(c.get('pacing', 'standard').lower(), 1.0) c['estimated_words'] = max(300, int(c.get('estimated_words', 0) * base_factor * pw)) adjusted_sum = sum(c['estimated_words'] for c in plan) if adjusted_sum > 0: norm = target_val / adjusted_sum for c in plan: c['estimated_words'] = max(300, int(c['estimated_words'] * norm)) utils.log("ARCHITECT", f"Chapter lengths scaled by pacing. Total ≈ {sum(c['estimated_words'] for c in plan)} words across {len(plan)} chapters.") return plan except Exception as e: utils.log("ARCHITECT", f"Failed to create chapter plan: {e}") return []