diff --git a/config.py b/config.py index a13d2b7..883f895 100644 --- a/config.py +++ b/config.py @@ -65,4 +65,4 @@ LENGTH_DEFINITIONS = { } # --- SYSTEM --- -VERSION = "1.3.0" \ No newline at end of file +VERSION = "1.3.1" \ No newline at end of file diff --git a/main.py b/main.py index b3730ae..dbac2bc 100644 --- a/main.py +++ b/main.py @@ -213,12 +213,17 @@ def process_book(bp, folder, context="", resume=False, interactive=False): pacing = story.check_pacing(bp, summary, txt, ch, remaining, folder) if pacing and pacing.get('status') == 'add_bridge': new_data = pacing.get('new_chapter', {}) + # Estimate bridge chapter length from current plan average (not hardcoded) + if chapters: + avg_words = int(sum(c.get('estimated_words', 1500) for c in chapters) / len(chapters)) + else: + avg_words = 1500 new_ch = { "chapter_number": ch['chapter_number'] + 1, "title": new_data.get('title', 'Bridge Chapter'), "pov_character": new_data.get('pov_character', ch.get('pov_character')), "pacing": "Slow", - "estimated_words": 1500, + "estimated_words": avg_words, "beats": new_data.get('beats', []) } chapters.insert(i+1, new_ch) diff --git a/modules/story.py b/modules/story.py index 74d66b0..64cf00e 100644 --- a/modules/story.py +++ b/modules/story.py @@ -260,30 +260,41 @@ def plan_structure(bp, folder): 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}") - - beats_context = [] - if not beats_context: - beats_context = bp.get('plot_beats', []) + # If events already well exceed the target, only deepen descriptions — don't add more + 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: Expand the outline to fit a {target_chapters}-chapter book. - CURRENT_COUNT: {len(events)} beats. - - INPUT_OUTLINE: - {json.dumps(beats_context)} - + TASK: {task} + + ORIGINAL_USER_BEATS (must all remain present): + {json.dumps(original_beats)} + CURRENT_EVENTS: {json.dumps(events)} - + RULES: - 1. Detect pacing gaps. - 2. Insert intermediate events. - 3. Deepen subplots. - 4. PRESERVE original beats. - - OUTPUT_FORMAT (JSON): {{ "events": [{{ "description": "String", "purpose": "String" }}] }} + 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.model_logic.generate_content(prompt) @@ -322,24 +333,30 @@ def create_chapter_plan(events, bp, folder): prompt = f""" ROLE: Pacing Specialist - TASK: Group events into Chapters. - - CONSTRAINTS: - - TARGET_CHAPTERS: {target} - - TARGET_WORDS: {words} - - INSTRUCTIONS: + 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. + (e.g. a tightly plotted thriller may need fewer; an epic with many subplots may need more.) + - TARGET_WORDS for the whole book: {words} + - Assign pacing to each chapter: Very Fast / Fast / Standard / Slow / Very Slow + Reflect dramatic rhythm — action scenes run fast, emotional beats run 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"] }}] + + OUTPUT_FORMAT (JSON): [{{"chapter_number": 1, "title": "String", "pov_character": "String", "pacing": "String", "estimated_words": 2000, "beats": ["String"]}}] """ try: response = ai.model_logic.generate_content(prompt) utils.log_usage(folder, ai.model_logic.name, response.usage_metadata) plan = json.loads(utils.clean_json(response.text)) - + + # Parse target word count target_str = str(words).lower().replace(',', '').replace('k', '000').replace('+', '').replace(' ', '') target_val = 0 if '-' in target_str: @@ -350,19 +367,34 @@ def create_chapter_plan(events, bp, folder): else: try: target_val = int(target_str) except: pass - + if target_val > 0: - variance = random.uniform(0.90, 1.10) + variance = random.uniform(0.92, 1.08) target_val = int(target_val * variance) - utils.log("ARCHITECT", f"Target adjusted with variance ({variance:.2f}x): {target_val} words.") + 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: - factor = target_val / current_sum - utils.log("ARCHITECT", f"Adjusting chapter lengths by {factor:.2f}x to match target.") + base_factor = target_val / current_sum + # Pacing multipliers — fast chapters are naturally shorter, slow chapters longer + pacing_weight = { + 'very fast': 0.60, 'fast': 0.80, 'standard': 1.00, + 'slow': 1.25, 'very slow': 1.50 + } + # Two-pass: apply pacing weights then normalise to hit total target for c in plan: - c['estimated_words'] = int(c.get('estimated_words', 0) * factor) - + 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)) + + # Normalise to keep total close to target + 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}") @@ -785,7 +817,7 @@ def write_chapter(chap, bp, folder, prev_sum, tracking=None, prev_content=None): {history_str} HARD_CONSTRAINTS: - - TARGET_WORDS: ~{est_words} (maintain this length — do not condense or summarise) + - TARGET_WORDS: ~{est_words} words (aim for this; ±20% is acceptable if the scene genuinely demands it — but do not condense beats to save space) - BEATS MUST BE COVERED: {json.dumps(chap.get('beats', []))} - SUMMARY CONTEXT: {prev_sum[:1500]}