diff --git a/main.py b/main.py index 3c5e46e..0aa4eba 100644 --- a/main.py +++ b/main.py @@ -106,7 +106,8 @@ def process_book(bp, folder, context="", resume=False): session_chapters = 0 session_time = 0 - for i in range(len(ms), len(chapters)): + i = len(ms) + while i < len(chapters): ch_start = time.time() ch = chapters[i] @@ -165,6 +166,38 @@ def process_book(bp, folder, context="", resume=False): with open(chars_track_path, "w") as f: json.dump(tracking['characters'], f, indent=2) with open(warn_track_path, "w") as f: json.dump(tracking.get('content_warnings', []), f, indent=2) + # --- DYNAMIC PACING CHECK --- + remaining = chapters[i+1:] + if remaining: + pacing = story.check_pacing(bp, summary, txt, ch, remaining, folder) + if pacing and pacing.get('status') == 'add_bridge': + new_data = pacing.get('new_chapter', {}) + 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, + "beats": new_data.get('beats', []) + } + chapters.insert(i+1, new_ch) + # Renumber subsequent chapters + for k in range(i+1, len(chapters)): chapters[k]['chapter_number'] = k + 1 + + with open(chapters_path, "w") as f: json.dump(chapters, f, indent=2) + utils.log("ARCHITECT", f" -> ⚠️ Pacing Intervention: Added bridge chapter '{new_ch['title']}' to fix rushing.") + + elif pacing and pacing.get('status') == 'cut_next': + removed = chapters.pop(i+1) + # Renumber subsequent chapters + for k in range(i+1, len(chapters)): chapters[k]['chapter_number'] = k + 1 + + with open(chapters_path, "w") as f: json.dump(chapters, f, indent=2) + utils.log("ARCHITECT", f" -> ⚠️ Pacing Intervention: Removed redundant chapter '{removed['title']}'.") + + # Increment loop + i += 1 + duration = time.time() - ch_start session_chapters += 1 session_time += duration diff --git a/modules/story.py b/modules/story.py index f32472d..b6e06b1 100644 --- a/modules/story.py +++ b/modules/story.py @@ -365,19 +365,23 @@ def evaluate_chapter_quality(text, chapter_title, model, folder): - Stilted Dialogue: Characters speaking in perfect paragraphs without interruptions, slang, or subtext. - White Room Syndrome: Dialogue occurring in a void without interaction with the setting/props. - "As You Know, Bob": Characters explaining things to each other that they both already know. + - Summary Mode: Summarizing conversation or action instead of dramatizing it (e.g. "They discussed the plan" vs writing the dialogue). CRITERIA: - 1. VOICE & TONE: Is the narrative voice distinct, or generic? Does it match the genre? - 2. SHOW, DON'T TELL: Are emotions demonstrated through action/viscera, or summarized? - 3. PACING: Does the scene drag? Is there conflict in every beat? - 4. CHARACTER AGENCY: Do characters make choices, or do things just happen to them? + 1. ENGAGEMENT & TENSION: Does the story grip the reader from the first line? Is there conflict or tension in every scene? + 2. SCENE EXECUTION: Is the middle of the chapter fully fleshed out? Does it avoid "sagging" or summarizing key moments? + 3. VOICE & TONE: Is the narrative voice distinct? Does it match the genre? + 4. SENSORY IMMERSION: Does the text engage all five senses (smell, sound, touch, etc.)? + 5. SHOW, DON'T TELL: Are emotions shown through physical reactions and subtext? + 6. CHARACTER AGENCY: Do characters drive the plot through active choices? + 7. PACING: Does the chapter feel rushed? Does the ending land with impact, or does it cut off too abruptly? Rate on a scale of 1-10. (Be harsh. 10 is Pulitzer level. 6 is average. Anything below 8 needs work). Return JSON: {{ 'score': int, 'critique': 'Detailed analysis of flaws, citing specific examples from the text.', - 'actionable_feedback': 'List of 3-5 specific, ruthless instructions for the rewrite (e.g. "Cut the first 3 paragraphs", "Make the dialogue in the middle argument more aggressive", "Describe the smell of the room").' + 'actionable_feedback': 'List of 3-5 specific, ruthless instructions for the rewrite (e.g. "Expand the middle dialogue", "Add sensory details about the rain", "Dramatize the argument instead of summarizing it").' }} """ try: @@ -393,6 +397,53 @@ def evaluate_chapter_quality(text, chapter_title, model, folder): except Exception as e: return 0, f"Evaluation error: {str(e)}" +def check_pacing(bp, summary, last_chapter_text, last_chapter_data, remaining_chapters, folder): + utils.log("ARCHITECT", "Checking pacing and structure health...") + + if not remaining_chapters: + return None + + meta = bp.get('book_metadata', {}) + genre = meta.get('genre', 'Fiction') + + prompt = f""" + Act as a Senior Structural Editor. + We just finished Chapter {last_chapter_data['chapter_number']}: "{last_chapter_data['title']}". + + STORY SO FAR (Summary): + {summary[-3000:]} + + JUST WRITTEN (Last 2000 chars): + {last_chapter_text[-2000:]} + + UPCOMING CHAPTERS (Next 3): + {json.dumps([c['title'] for c in remaining_chapters[:3]])} + + TOTAL REMAINING: {len(remaining_chapters)} chapters. + + ANALYSIS TASK: + Determine if the story is moving too fast (Rushed) or too slow (Dragging) based on the {genre} genre. + + DECISION RULES: + - If the last chapter skipped over major emotional reactions, travel, or necessary setup -> ADD_BRIDGE. + - If the last chapter already covered the events of the NEXT chapter -> CUT_NEXT. + - If the pacing is fine -> OK. + + RETURN JSON: + {{ + "status": "ok" or "add_bridge" or "cut_next", + "reason": "Explanation...", + "new_chapter": {{ "title": "...", "beats": ["..."], "pov_character": "..." }} (Required if add_bridge) + }} + """ + try: + response = ai.model_logic.generate_content(prompt) + utils.log_usage(folder, "logic-pro", response.usage_metadata) + return json.loads(utils.clean_json(response.text)) + except Exception as e: + utils.log("ARCHITECT", f"Pacing check failed: {e}") + return None + def create_initial_persona(bp, folder): utils.log("SYSTEM", "Generating initial Author Persona based on genre/tone...") meta = bp.get('book_metadata', {}) @@ -618,6 +669,7 @@ def write_chapter(chap, bp, folder, prev_sum, tracking=None, prev_content=None): 3. SUBTEXT: Ensure dialogue implies meaning rather than stating it outright. People rarely say exactly what they mean. 4. GENRE CONSISTENCY: Ensure the tone matches {meta.get('genre', 'Fiction')}. 5. SETTING INTERACTION: Ensure characters interact with their environment (props, weather, lighting) during dialogue. + 6. DRAMATIZE, DON'T SUMMARIZE: Expand summarized moments into full scenes with dialogue and action. Ensure the scene feels "full" and immersive. STORY SO FAR: {prev_sum} diff --git a/templates/project.html b/templates/project.html index 5943339..41025d3 100644 --- a/templates/project.html +++ b/templates/project.html @@ -459,6 +459,7 @@ let activeInterval = null; // Only auto-poll if we have a latest run let currentRunId = {{ active_run.id if active_run else 'null' }}; + const initialRunStatus = "{{ active_run.status if active_run else '' }}"; function fetchLog() { if (!currentRunId) return; @@ -509,8 +510,9 @@ } else { if (activeInterval) clearInterval(activeInterval); activeInterval = null; - // Reload page on completion to show download buttons - if (data.status === 'completed' && !document.querySelector('.alert-success')) { + + // Reload if we were polling (watched it finish) OR if page loaded as running but is now done + if (initialRunStatus === 'running' || initialRunStatus === 'queued') { window.location.reload(); } } diff --git a/templates/run_details.html b/templates/run_details.html index 2132955..b015d44 100644 --- a/templates/run_details.html +++ b/templates/run_details.html @@ -260,6 +260,7 @@