import json import os from core import utils from ai import models as ai_models from story.style_persona import get_style_guidelines def evaluate_chapter_quality(text, chapter_title, genre, model, folder): guidelines = get_style_guidelines() ai_isms = "', '".join(guidelines['ai_isms']) fw_examples = ", ".join([f"'He {w}'" for w in guidelines['filter_words'][:5]]) word_count = len(text.split()) if text else 0 min_sugg = max(3, int(word_count / 500)) max_sugg = min_sugg + 2 suggestion_range = f"{min_sugg}-{max_sugg}" prompt = f""" ROLE: Senior Literary Editor TASK: Critique chapter draft. METADATA: - TITLE: {chapter_title} - GENRE: {genre} PROHIBITED_PATTERNS: - AI_ISMS: {ai_isms} - FILTER_WORDS: {fw_examples} - CLICHES: White Room, As You Know Bob, Summary Mode, Anachronisms. - SYNTAX: Repetitive structure, Passive Voice, Adverb Reliance. QUALITY_RUBRIC (1-10): 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 use sensory details effectively without being overwhelming? 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? 8. GENRE APPROPRIATENESS: Are introductions of characters, places, items, or actions consistent with the {genre} conventions? 9. DIALOGUE AUTHENTICITY: Do characters sound distinct? Is there subtext? Avoids "on-the-nose" dialogue. 10. PLOT RELEVANCE: Does the chapter advance the plot or character arcs significantly? Avoids filler. 11. STAGING & FLOW: Do characters enter/exit physically? Do paragraphs transition logically (Action -> Reaction)? 12. PROSE DYNAMICS: Is there sentence variety? Avoids purple prose, adjective stacking, and excessive modification. 13. CLARITY & READABILITY: Is the text easy to follow? Are sentences clear and concise? SCORING_SCALE: - 10 (Masterpiece): Flawless, impactful, ready for print. - 9 (Bestseller): Exceptional quality, minor style tweaks only. - 7-8 (Professional): Good draft, solid structure, needs editing. - 6 (Passable): Average, has issues with pacing or voice. Needs heavy refinement. - 1-5 (Fail): Structural flaws, boring, or incoherent. Needs rewrite. OUTPUT_FORMAT (JSON): {{ "score": int, "critique": "Detailed analysis of flaws, citing specific examples from the text.", "actionable_feedback": "List of {suggestion_range} 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: response = model.generate_content([prompt, text[:30000]]) model_name = getattr(model, 'name', ai_models.logic_model_name) utils.log_usage(folder, model_name, response.usage_metadata) data = json.loads(utils.clean_json(response.text)) critique_text = data.get('critique', 'No critique provided.') if data.get('actionable_feedback'): critique_text += "\n\nREQUIRED FIXES:\n" + str(data.get('actionable_feedback')) return data.get('score', 0), critique_text 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', {}) prompt = f""" ROLE: Structural Editor TASK: Analyze pacing. CONTEXT: - PREVIOUS_SUMMARY: {summary[-3000:]} - CURRENT_CHAPTER: {last_chapter_text[-2000:]} - UPCOMING: {json.dumps([c['title'] for c in remaining_chapters[:3]])} - REMAINING_COUNT: {len(remaining_chapters)} LOGIC: - IF skipped major beats -> ADD_BRIDGE - IF covered next chapter's beats -> CUT_NEXT - ELSE -> OK OUTPUT_FORMAT (JSON): {{ "status": "ok" or "add_bridge" or "cut_next", "reason": "Explanation...", "new_chapter": {{ "title": "...", "beats": ["..."], "pov_character": "..." }} (Required if add_bridge) }} """ 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)) except Exception as e: utils.log("ARCHITECT", f"Pacing check failed: {e}") return None def analyze_consistency(bp, manuscript, folder): utils.log("EDITOR", "Analyzing manuscript for continuity errors...") if not manuscript: return {"issues": ["No manuscript found."], "score": 0} if not bp: return {"issues": ["No blueprint found."], "score": 0} chapter_summaries = [] for ch in manuscript: text = ch.get('content', '') excerpt = text[:1000] + "\n...\n" + text[-1000:] if len(text) > 2000 else text chapter_summaries.append(f"Ch {ch.get('num')}: {excerpt}") context = "\n".join(chapter_summaries) prompt = f""" ROLE: Continuity Editor TASK: Analyze book summary for plot holes. INPUT_DATA: - CHARACTERS: {json.dumps(bp.get('characters', []))} - SUMMARIES: {context} OUTPUT_FORMAT (JSON): {{ "issues": ["Issue 1", "Issue 2"], "score": 8, "summary": "Brief overall assessment." }} """ 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)) except Exception as e: return {"issues": [f"Analysis failed: {e}"], "score": 0, "summary": "Error during analysis."} def rewrite_chapter_content(bp, manuscript, chapter_num, instruction, folder): utils.log("WRITER", f"Rewriting Ch {chapter_num} with instruction: {instruction}") target_chap = next((c for c in manuscript if str(c.get('num')) == str(chapter_num)), None) if not target_chap: return None prev_text = "" prev_chap = None if isinstance(chapter_num, int): prev_chap = next((c for c in manuscript if c['num'] == chapter_num - 1), None) elif str(chapter_num).lower() == "epilogue": numbered_chaps = [c for c in manuscript if isinstance(c['num'], int)] if numbered_chaps: prev_chap = max(numbered_chaps, key=lambda x: x['num']) if prev_chap: prev_text = prev_chap.get('content', '')[-3000:] meta = bp.get('book_metadata', {}) ad = meta.get('author_details', {}) if not ad and 'author_bio' in meta: persona_info = meta['author_bio'] else: persona_info = f"Name: {ad.get('name', meta.get('author', 'Unknown'))}\n" if ad.get('bio'): persona_info += f"Style/Bio: {ad['bio']}\n" char_visuals = "" from core import config tracking_path = os.path.join(folder, "tracking_characters.json") if os.path.exists(tracking_path): try: tracking_chars = utils.load_json(tracking_path) if tracking_chars: char_visuals = "\nCHARACTER TRACKING (Visuals & Preferences):\n" for name, data in tracking_chars.items(): desc = ", ".join(data.get('descriptors', [])) speech = data.get('speech_style', 'Unknown') char_visuals += f"- {name}: {desc}\n * Speech: {speech}\n" except: pass guidelines = get_style_guidelines() fw_list = '", "'.join(guidelines['filter_words']) prompt = f""" You are an expert fiction writing AI. Your task is to rewrite a specific chapter based on a user directive. INPUT DATA: - TITLE: {meta.get('title')} - GENRE: {meta.get('genre')} - TONE: {meta.get('style', {}).get('tone')} - AUTHOR_VOICE: {persona_info} - PREVIOUS_CONTEXT: {prev_text} - CURRENT_DRAFT: {target_chap.get('content', '')[:5000]} - CHARACTERS: {json.dumps(bp.get('characters', []))} {char_visuals} PRIMARY DIRECTIVE (USER INSTRUCTION): {instruction} EXECUTION RULES: 1. CONTINUITY: The new text must flow logically from PREVIOUS_CONTEXT. 2. ADHERENCE: The PRIMARY DIRECTIVE overrides any conflicting details in CURRENT_DRAFT. 3. VOICE: Strictly emulate the AUTHOR_VOICE. 4. GENRE: Enforce {meta.get('genre')} conventions. No anachronisms. 5. LOGIC: Enforce strict causality (Action -> Reaction). No teleporting characters. PROSE OPTIMIZATION RULES (STRICT ENFORCEMENT): - FILTER_REMOVAL: Scan for words [{fw_list}]. If found, rewrite the sentence to remove the filter and describe the sensation directly. - SENTENCE_VARIETY: Penalize consecutive sentences starting with the same pronoun or article. Vary structure. - SHOW_DONT_TELL: Convert internal summaries of emotion into physical actions or subtextual dialogue. - ACTIVE_VOICE: Convert passive voice ("was [verb]ed") to active voice. - SENSORY_ANCHORING: The first paragraph must establish the setting using at least one non-visual sense (smell, sound, touch). - SUBTEXT: Dialogue must imply meaning rather than stating it outright. RETURN JSON: {{ "content": "The full chapter text in Markdown...", "summary": "A concise summary of the chapter's events and ending state (for continuity checks)." }} """ try: response = ai_models.model_logic.generate_content(prompt) utils.log_usage(folder, ai_models.model_logic.name, response.usage_metadata) try: data = json.loads(utils.clean_json(response.text)) return data.get('content'), data.get('summary') except: return response.text, None except Exception as e: utils.log("WRITER", f"Rewrite failed: {e}") return None, None def check_and_propagate(bp, manuscript, changed_chap_num, folder, change_summary=None): utils.log("WRITER", f"Checking ripple effects from Ch {changed_chap_num}...") changed_chap = next((c for c in manuscript if c['num'] == changed_chap_num), None) if not changed_chap: return None if change_summary: current_context = change_summary else: change_summary_prompt = f""" ROLE: Summarizer TASK: Summarize the key events and ending state of this chapter for continuity tracking. TEXT: {changed_chap.get('content', '')[:10000]} FOCUS: - Major plot points. - Character status changes (injuries, items acquired, location changes). - New information revealed. OUTPUT: Concise text summary. """ try: resp = ai_models.model_writer.generate_content(change_summary_prompt) utils.log_usage(folder, ai_models.model_writer.name, resp.usage_metadata) current_context = resp.text except: current_context = changed_chap.get('content', '')[-2000:] original_change_context = current_context sorted_ms = sorted(manuscript, key=utils.chapter_sort_key) start_index = -1 for i, c in enumerate(sorted_ms): if str(c['num']) == str(changed_chap_num): start_index = i break if start_index == -1 or start_index == len(sorted_ms) - 1: return None changes_made = False consecutive_no_changes = 0 potential_impact_chapters = [] for i in range(start_index + 1, len(sorted_ms)): target_chap = sorted_ms[i] if consecutive_no_changes >= 2: if target_chap['num'] not in potential_impact_chapters: future_flags = [n for n in potential_impact_chapters if isinstance(n, int) and isinstance(target_chap['num'], int) and n > target_chap['num']] if not future_flags: remaining_chaps = sorted_ms[i:] if not remaining_chaps: break utils.log("WRITER", " -> Short-term ripple dissipated. Scanning remaining chapters for long-range impacts...") chapter_summaries = [] for rc in remaining_chaps: text = rc.get('content', '') excerpt = text[:500] + "\n...\n" + text[-500:] if len(text) > 1000 else text chapter_summaries.append(f"Ch {rc['num']}: {excerpt}") scan_prompt = f""" ROLE: Continuity Scanner TASK: Identify chapters impacted by a change. CHANGE_CONTEXT: {original_change_context} CHAPTER_SUMMARIES: {json.dumps(chapter_summaries)} CRITERIA: Identify later chapters that mention items, characters, or locations involved in the Change Context. OUTPUT_FORMAT (JSON): [Chapter_Number_Int, ...] """ try: resp = ai_models.model_logic.generate_content(scan_prompt) utils.log_usage(folder, ai_models.model_logic.name, resp.usage_metadata) potential_impact_chapters = json.loads(utils.clean_json(resp.text)) if not isinstance(potential_impact_chapters, list): potential_impact_chapters = [] potential_impact_chapters = [int(x) for x in potential_impact_chapters if str(x).isdigit()] except Exception as e: utils.log("WRITER", f" -> Scan failed: {e}. Stopping.") break if not potential_impact_chapters: utils.log("WRITER", " -> No long-range impacts detected. Stopping.") break else: utils.log("WRITER", f" -> Detected potential impact in chapters: {potential_impact_chapters}") if isinstance(target_chap['num'], int) and target_chap['num'] not in potential_impact_chapters: utils.log("WRITER", f" -> Skipping Ch {target_chap['num']} (Not flagged).") continue utils.log("WRITER", f" -> Checking Ch {target_chap['num']} for continuity...") chap_word_count = len(target_chap.get('content', '').split()) prompt = f""" ROLE: Continuity Checker TASK: Determine if a chapter contradicts a story change. If it does, rewrite it to fix the contradiction. CHANGED_CHAPTER: {changed_chap_num} CHANGE_SUMMARY: {current_context} CHAPTER_TO_CHECK (Ch {target_chap['num']}): {target_chap['content'][:12000]} DECISION_LOGIC: - If the chapter directly contradicts the change (references dead characters, items that no longer exist, events that didn't happen), status = REWRITE. - If the chapter is consistent or only tangentially related, status = NO_CHANGE. - Be conservative — only rewrite if there is a genuine contradiction. REWRITE_RULES (apply only if REWRITE): - Fix the specific contradiction. Preserve all other content. - The rewritten chapter MUST be approximately {chap_word_count} words (same length as original). - Include the chapter header formatted as Markdown H1. - Do not add new plot points not in the original. OUTPUT_FORMAT (JSON): {{ "status": "NO_CHANGE" or "REWRITE", "reason": "Brief explanation of the contradiction or why it's consistent", "content": "Full Markdown rewritten chapter (ONLY if status is REWRITE, otherwise null)" }} """ try: response = ai_models.model_writer.generate_content(prompt) utils.log_usage(folder, ai_models.model_writer.name, response.usage_metadata) data = json.loads(utils.clean_json(response.text)) if data.get('status') == 'NO_CHANGE': utils.log("WRITER", f" -> Ch {target_chap['num']} is consistent.") current_context = f"Ch {target_chap['num']} Summary: " + target_chap.get('content', '')[-2000:] consecutive_no_changes += 1 elif data.get('status') == 'REWRITE' and data.get('content'): new_text = data.get('content') if new_text: utils.log("WRITER", f" -> Rewriting Ch {target_chap['num']} to fix continuity.") target_chap['content'] = new_text changes_made = True current_context = f"Ch {target_chap['num']} Summary: " + new_text[-2000:] consecutive_no_changes = 0 try: with open(os.path.join(folder, "manuscript.json"), 'w') as f: json.dump(manuscript, f, indent=2) except: pass except Exception as e: utils.log("WRITER", f" -> Check failed: {e}") return manuscript if changes_made else None