From 7e5dbe6f00c7aaec7b5414711724dcad69b3607a Mon Sep 17 00:00:00 2001 From: Mike Wichers Date: Thu, 5 Feb 2026 22:26:55 -0500 Subject: [PATCH] Strengthened writing. --- main.py | 38 +-- modules/ai.py | 25 +- modules/marketing.py | 86 +++++-- modules/story.py | 586 +++++++++++++++++++++++++++---------------- modules/web_app.py | 86 ++++--- modules/web_tasks.py | 7 +- wizard.py | 99 ++++---- 7 files changed, 577 insertions(+), 350 deletions(-) diff --git a/main.py b/main.py index eab9ea7..5bf1335 100644 --- a/main.py +++ b/main.py @@ -100,7 +100,14 @@ def process_book(bp, folder, context="", resume=False, interactive=False): utils.log("RESUME", "Rebuilding 'Story So Far' from existing manuscript...") try: combined_text = "\n".join([f"Chapter {c['num']}: {c['content']}" for c in ms]) - resp_sum = ai.model_writer.generate_content(f"Create a detailed, cumulative 'Story So Far' summary from the following text. Use dense, factual bullet points. Focus on character meetings, relationships, and known information:\n{combined_text}") + resp_sum = ai.model_writer.generate_content(f""" + ROLE: Series Historian + TASK: Create a cumulative 'Story So Far' summary. + INPUT_TEXT: + {combined_text} + INSTRUCTIONS: Use dense, factual bullet points. Focus on character meetings, relationships, and known information. + OUTPUT: Summary text. + """) utils.log_usage(folder, "writer-flash", resp_sum.usage_metadata) summary = resp_sum.text except: summary = "The story continues." @@ -161,29 +168,30 @@ def process_book(bp, folder, context="", resume=False, interactive=False): try: update_prompt = f""" - Update the 'Story So Far' summary to include the events of this new chapter. + ROLE: Series Historian + TASK: Update the 'Story So Far' summary to include the events of this new chapter. - STYLE: Dense, factual, chronological bullet points. Avoid narrative prose. - GOAL: Maintain a perfect memory of the plot for continuity. - - CRITICAL INSTRUCTIONS: - 1. CUMULATIVE: Do NOT remove old events. Append and integrate new information. - 2. TRACKING: Explicitly note who met whom, who knows what, and current locations. - 3. RELEVANCE: Ensure details needed for the UPCOMING CONTEXT are preserved. - - CURRENT STORY SO FAR: + INPUT_DATA: + - CURRENT_SUMMARY: {summary} - - NEW CHAPTER CONTENT: + - NEW_CHAPTER_TEXT: {txt} - {next_info} + - UPCOMING_CONTEXT_HINT: {next_info} + + INSTRUCTIONS: + 1. STYLE: Dense, factual, chronological bullet points. Avoid narrative prose. + 2. CUMULATIVE: Do NOT remove old events. Append and integrate new information. + 3. TRACKING: Explicitly note who met whom, who knows what, and current locations. + 4. RELEVANCE: Ensure details needed for the UPCOMING CONTEXT are preserved. + + OUTPUT: Updated summary text. """ resp_sum = ai.model_writer.generate_content(update_prompt) utils.log_usage(folder, "writer-flash", resp_sum.usage_metadata) summary = resp_sum.text except: try: - resp_fallback = ai.model_writer.generate_content(f"Summarize plot points:\n{txt}") + resp_fallback = ai.model_writer.generate_content(f"ROLE: Summarizer\nTASK: Summarize plot points.\nTEXT: {txt}\nOUTPUT: Bullet points.") utils.log_usage(folder, "writer-flash", resp_fallback.usage_metadata) summary += f"\n\nChapter {ch['chapter_number']}: " + resp_fallback.text except: summary += f"\n\nChapter {ch['chapter_number']}: [Content processed]" diff --git a/modules/ai.py b/modules/ai.py index 717b7fb..4bcfcf9 100644 --- a/modules/ai.py +++ b/modules/ai.py @@ -85,7 +85,30 @@ def select_best_models(force_refresh=False): utils.log("SYSTEM", f"Bootstrapping model selection with: {bootstrapper}") model = genai.GenerativeModel(bootstrapper) - prompt = f"Analyze this list of available Google Gemini models:\n{json.dumps(models)}\n\nSelect the best model for each of these three roles based on these criteria:\n- Most recent version with best features and ability.\n- Beta versions are okay, but avoid 'experimental' if a stable beta/prod version exists.\n- Consider quota efficiency (Flash is cheaper/faster, Pro is smarter).\n\nROLES:\n1. LOGIC: For complex reasoning, JSON structuring, and plot planning.\n2. WRITER: For creative fiction writing, prose generation, and speed.\n3. ARTIST: For generating visual art prompts and design instructions.\n\nAlso provide a 'ranking' list of ALL models analyzed, ordered from best/most useful to worst/least useful, with a short reason.\n\nReturn JSON: {{ 'logic': {{ 'model': 'model_name', 'reason': 'reasoning' }}, 'writer': {{ 'model': 'model_name', 'reason': 'reasoning' }}, 'artist': {{ 'model': 'model_name', 'reason': 'reasoning' }}, 'ranking': [ {{ 'model': 'model_name', 'reason': 'reasoning' }} ] }}" + prompt = f""" + ROLE: AI Model Architect + TASK: Select the optimal Gemini models for specific application roles. + + AVAILABLE_MODELS: + {json.dumps(models)} + + CRITERIA: + - LOGIC: Needs complex reasoning, JSON adherence, and instruction following. (Prefer Pro/1.5). + - WRITER: Needs creativity, prose quality, and speed. (Prefer Flash/1.5 for speed, or Pro for quality). + - ARTIST: Needs visual prompt understanding. + + CONSTRAINTS: + - Avoid 'experimental' unless no stable version exists. + - Prioritize 'latest' or stable versions. + + OUTPUT_FORMAT (JSON): + {{ + "logic": {{ "model": "string", "reason": "string" }}, + "writer": {{ "model": "string", "reason": "string" }}, + "artist": {{ "model": "string", "reason": "string" }}, + "ranking": [ {{ "model": "string", "reason": "string" }} ] + }} + """ try: response = model.generate_content(prompt) diff --git a/modules/marketing.py b/modules/marketing.py index c816c77..d783dcc 100644 --- a/modules/marketing.py +++ b/modules/marketing.py @@ -74,7 +74,12 @@ def evaluate_image_quality(image_path, prompt, model, folder=None): if not HAS_PIL: return None, "PIL not installed" try: img = Image.open(image_path) - response = model.generate_content([f"Analyze this generated image against the description: '{prompt}'.\nRate accuracy/relevance on a scale of 1-10.\nProvide a 1-sentence critique.\nReturn JSON: {{'score': int, 'reason': 'string'}}", img]) + response = model.generate_content([f""" + ROLE: Art Critic + TASK: Analyze generated image against prompt. + PROMPT: '{prompt}' + OUTPUT_FORMAT (JSON): {{ "score": int (1-10), "reason": "string" }} + """, img]) if folder: utils.log_usage(folder, "logic-pro", response.usage_metadata) data = json.loads(utils.clean_json(response.text)) return data.get('score'), data.get('reason') @@ -85,12 +90,17 @@ def generate_blurb(bp, folder): meta = bp.get('book_metadata', {}) prompt = f""" - Write a compelling back-cover blurb (approx 150-200 words) for this book. - TITLE: {meta.get('title')} - GENRE: {meta.get('genre')} - LOGLINE: {bp.get('manual_instruction')} - PLOT: {json.dumps(bp.get('plot_beats', []))} - CHARACTERS: {json.dumps(bp.get('characters', []))} + ROLE: Marketing Copywriter + TASK: Write a back-cover blurb (150-200 words). + + INPUT_DATA: + - TITLE: {meta.get('title')} + - GENRE: {meta.get('genre')} + - LOGLINE: {bp.get('manual_instruction')} + - PLOT: {json.dumps(bp.get('plot_beats', []))} + - CHARACTERS: {json.dumps(bp.get('characters', []))} + + OUTPUT: Text only. """ try: response = ai.model_writer.generate_content(prompt) @@ -134,13 +144,16 @@ def generate_cover(bp, folder, tracking=None, feedback=None, interactive=False): if feedback and feedback.strip(): utils.log("MARKETING", f"Analyzing feedback: '{feedback}'...") analysis_prompt = f""" - User Feedback on Book Cover: "{feedback}" - Determine if the user wants to: + ROLE: Design Assistant + TASK: Analyze user feedback on cover. + + FEEDBACK: "{feedback}" + + DECISION: 1. Keep the current background image but change text/layout/color (REGENERATE_LAYOUT). 2. Create a completely new background image (REGENERATE_IMAGE). - NOTE: If the feedback is generic (e.g. "regenerate", "try again") or does not explicitly mention keeping the image/changing text only, default to REGENERATE_IMAGE. - Return JSON: {{ "action": "REGENERATE_LAYOUT" or "REGENERATE_IMAGE", "instruction": "Specific instruction for the Art Director" }} + OUTPUT_FORMAT (JSON): {{ "action": "REGENERATE_LAYOUT" or "REGENERATE_IMAGE", "instruction": "Specific instruction for Art Director" }} """ try: resp = ai.model_logic.generate_content(analysis_prompt) @@ -153,20 +166,24 @@ def generate_cover(bp, folder, tracking=None, feedback=None, interactive=False): utils.log("MARKETING", "Feedback analysis failed. Defaulting to full regeneration.") design_prompt = f""" - Act as an Art Director. Design the cover for this book. - TITLE: {meta.get('title')} - GENRE: {meta.get('genre')} - TONE: {meta.get('style', {}).get('tone', 'Balanced')} + ROLE: Art Director + TASK: Design a book cover. - CRITICAL INSTRUCTIONS: - 1. CHARACTER APPEARANCE: Strictly adhere to the provided character descriptions (hair, eyes, race, age, clothing) in the Visual Context. - 2. GENRE EXPRESSIONS: Ensure character facial expressions and body language heavily reflect the GENRE (e.g. Horror = terrified/menacing, Romance = longing/soft, Thriller = intense/alert). + METADATA: + - TITLE: {meta.get('title')} + - GENRE: {meta.get('genre')} + - TONE: {meta.get('style', {}).get('tone', 'Balanced')} + VISUAL_CONTEXT: {visual_context} - {f"USER FEEDBACK: {feedback}" if feedback else ""} - {f"INSTRUCTION: {design_instruction}" if design_instruction else ""} - Provide JSON output: + USER_FEEDBACK: + {f"{feedback}" if feedback else "None"} + + INSTRUCTION: + {f"{design_instruction}" if design_instruction else "Create a compelling, genre-appropriate cover."} + + OUTPUT_FORMAT (JSON): {{ "font_name": "Name of a popular Google Font (e.g. Roboto, Cinzel, Oswald, Playfair Display)", "primary_color": "#HexCode (Background)", @@ -277,10 +294,21 @@ def generate_cover(bp, folder, tracking=None, feedback=None, interactive=False): best_layout_path = None base_layout_prompt = f""" - Act as a Senior Book Cover Designer. Analyze this 600x900 cover art. - BOOK DETAILS: Title: {meta.get('title')}, Author: {meta.get('author')}, Genre: {meta.get('genre')} - TASK: Determine best (x, y) coordinates for Title and Author. Do NOT place text over faces. - RETURN JSON: {{ "title": {{ "x": int, "y": int, "font_size": int, "font_name": "String", "color": "#Hex" }}, "author": {{ "x": int, "y": int, "font_size": int, "font_name": "String", "color": "#Hex" }} }} + ROLE: Graphic Designer + TASK: Determine text layout coordinates for a 600x900 cover. + + METADATA: + - TITLE: {meta.get('title')} + - AUTHOR: {meta.get('author')} + - GENRE: {meta.get('genre')} + + CONSTRAINT: Do NOT place text over faces. + + OUTPUT_FORMAT (JSON): + {{ + "title": {{ "x": Int, "y": Int, "font_size": Int, "font_name": "String", "color": "#Hex" }}, + "author": {{ "x": Int, "y": Int, "font_size": Int, "font_name": "String", "color": "#Hex" }} + }} """ if feedback: @@ -344,7 +372,13 @@ def generate_cover(bp, folder, tracking=None, feedback=None, interactive=False): img_copy.save(attempt_path) # Evaluate Layout - eval_prompt = f"Analyze this book cover layout. Is the text legible? Is the contrast good? Does it look professional? Title: {meta.get('title')}" + eval_prompt = f""" + Analyze the text layout for the book title '{meta.get('title')}'. + CHECKLIST: + 1. Is the text legible against the background? + 2. Is the contrast sufficient? + 3. Does it look professional? + """ score, critique = evaluate_image_quality(attempt_path, eval_prompt, ai.model_logic, folder) if score is None: score = 0 diff --git a/modules/story.py b/modules/story.py index a3bf0ca..a52ccec 100644 --- a/modules/story.py +++ b/modules/story.py @@ -36,20 +36,19 @@ def refresh_style_guidelines(model, folder=None): current = get_style_guidelines() prompt = f""" - Act as a Literary Editor. Update our 'Banned Words' lists for AI writing. + ROLE: Literary Editor + TASK: Update 'Banned Words' lists for AI writing. - CURRENT AI-ISMS (Cliches to avoid): - {json.dumps(current.get('ai_isms', []))} + INPUT_DATA: + - CURRENT_AI_ISMS: {json.dumps(current.get('ai_isms', []))} + - CURRENT_FILTER_WORDS: {json.dumps(current.get('filter_words', []))} - CURRENT FILTER WORDS (Distancing language): - {json.dumps(current.get('filter_words', []))} - - TASK: - 1. Review the lists. Remove any that are too common/safe (false positives). + INSTRUCTIONS: + 1. Review lists. Remove false positives. 2. Add new common AI tropes (e.g. 'neon-lit', 'bustling', 'a sense of', 'mined', 'delved'). - 3. Ensure the list is robust but not paralyzing. + 3. Ensure robustness. - RETURN JSON: {{ "ai_isms": [strings], "filter_words": [strings] }} + OUTPUT_FORMAT (JSON): {{ "ai_isms": [strings], "filter_words": [strings] }} """ try: response = model.generate_content(prompt) @@ -131,25 +130,24 @@ def enrich(bp, folder, context=""): if 'plot_beats' not in bp: bp['plot_beats'] = [] prompt = f""" - You are a Creative Director. - The user has provided a minimal description. You must build a full Book Bible. + ROLE: Creative Director + TASK: Create a comprehensive Book Bible from the user description. - USER DESCRIPTION: "{bp.get('manual_instruction', 'A generic story')}" - CONTEXT (Sequel): {context} + INPUT DATA: + - USER_DESCRIPTION: "{bp.get('manual_instruction', 'A generic story')}" + - CONTEXT (Sequel): {context} - TASK: + 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. - - IF SEQUEL: Decide if we continue with previous protagonists or shift to side characters based on USER DESCRIPTION. - - IF NEW CHARACTERS: Create them. - - IF RETURNING: Reuse details from CONTEXT. + - 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"). - RETURN JSON in this EXACT format: + 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"] }} ], @@ -224,7 +222,16 @@ def plan_structure(bp, folder): if not beats_context: beats_context = bp.get('plot_beats', []) - prompt = f"{structure_type}\nTITLE: {bp['book_metadata']['title']}\nBEATS: {json.dumps(beats_context)}\nReturn JSON: {{'events': [{{'description':'...', 'purpose':'...'}}]}}" + prompt = f""" + ROLE: Story Architect + TASK: Create a structural event outline. + + ARCHETYPE: {structure_type} + TITLE: {bp['book_metadata']['title']} + EXISTING_BEATS: {json.dumps(beats_context)} + + OUTPUT_FORMAT (JSON): {{ "events": [{{ "description": "String", "purpose": "String" }}] }} + """ try: response = ai.model_logic.generate_content(prompt) utils.log_usage(folder, "logic-pro", response.usage_metadata) @@ -241,23 +248,23 @@ def expand(events, pass_num, target_chapters, bp, folder): beats_context = bp.get('plot_beats', []) prompt = f""" - You are a Story Architect. - Goal: Flesh out this outline for a {target_chapters}-chapter book. - Current Status: {len(events)} beats. + ROLE: Story Architect + TASK: Expand the outline to fit a {target_chapters}-chapter book. + CURRENT_COUNT: {len(events)} beats. - ORIGINAL OUTLINE: + INPUT_OUTLINE: {json.dumps(beats_context)} - INSTRUCTIONS: - 1. Look for jumps in time or logic. - 2. Insert new intermediate events to smooth the pacing. - 3. Deepen subplots while staying true to the ORIGINAL OUTLINE. - 4. Do NOT remove or drastically alter the original outline points; expand AROUND them. - - CURRENT EVENTS: + CURRENT_EVENTS: {json.dumps(events)} - Return JSON: {{'events': [ ...updated full list... ]}} + RULES: + 1. Detect pacing gaps. + 2. Insert intermediate events. + 3. Deepen subplots. + 4. PRESERVE original beats. + + OUTPUT_FORMAT (JSON): {{ "events": [{{ "description": "String", "purpose": "String" }}] }} """ try: response = ai.model_logic.generate_content(prompt) @@ -295,18 +302,19 @@ def create_chapter_plan(events, bp, folder): pov_instruction = f"- Assign a 'pov_character' for each chapter from this list: {json.dumps(pov_chars)}." prompt = f""" - Group events into Chapters. - TARGET CHAPTERS: {target} (Approximate. Feel free to adjust +/- 20% for better pacing). - TARGET WORDS: {words} (Total for the book). + ROLE: Pacing Specialist + TASK: Group events into Chapters. - INSTRUCTIONS: - - Vary chapter pacing. Options: 'Very Fast', 'Fast', 'Standard', 'Slow', 'Very Slow'. - - Assign an estimated word count to each chapter based on its pacing and content. + CONSTRAINTS: + - TARGET_CHAPTERS: {target} + - TARGET_WORDS: {words} + - INSTRUCTIONS: {structure_instructions} {pov_instruction} - EVENTS: {json.dumps(events)} - Return JSON: [{{'chapter_number':1, 'title':'...', 'pov_character': 'Name', 'pacing': 'Standard', 'estimated_words': 2000, 'beats':[...]}}] + 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.model_logic.generate_content(prompt) @@ -345,24 +353,26 @@ def update_tracking(folder, chapter_num, chapter_text, current_tracking): utils.log("TRACKER", f"Updating world state & character visuals for Ch {chapter_num}...") prompt = f""" - Analyze this chapter text to update the Story Bible. + ROLE: Continuity Tracker + TASK: Update the Story Bible based on the new chapter. - CURRENT TRACKING DATA: + INPUT_TRACKING: {json.dumps(current_tracking)} - NEW CHAPTER TEXT: + NEW_TEXT: {chapter_text[:500000]} - TASK: - 1. EVENTS: Append 1-3 concise bullet points summarizing key plot events in this chapter to the 'events' list. - 2. CHARACTERS: Update entries for any characters appearing in the scene. + OPERATIONS: + 1. EVENTS: Append 1-3 key plot points to 'events'. + 2. CHARACTERS: Update 'descriptors', 'likes_dislikes', 'speech_style', 'last_worn', 'major_events'. - "descriptors": List of strings. Add PERMANENT physical traits (height, hair, eyes), specific items (jewelry, weapons). Avoid duplicates. - "likes_dislikes": List of strings. Add specific preferences, likes, or dislikes mentioned (e.g., "Hates coffee", "Loves jazz"). + - "speech_style": String. Describe how they speak (e.g. "Formal, no contractions", "Uses slang", "Stutters", "Short sentences"). - "last_worn": String. Update if specific clothing is described. IMPORTANT: If a significant time jump occurred (e.g. next day) and no new clothing is described, reset this to "Unknown". - "major_events": List of strings. Log significant life-altering events occurring in THIS chapter (e.g. "Lost an arm", "Married", "Betrayed by X"). - 3. CONTENT_WARNINGS: List of strings. Identify specific triggers present in this chapter (e.g. "Graphic Violence", "Sexual Assault", "Torture", "Self-Harm"). Append to existing list. + 3. WARNINGS: Append new 'content_warnings'. - RETURN JSON with the SAME structure as CURRENT TRACKING DATA (events list, characters dict, content_warnings list). + OUTPUT_FORMAT (JSON): Return the updated tracking object structure. """ try: response = ai.model_logic.generate_content(prompt) @@ -378,20 +388,27 @@ def evaluate_chapter_quality(text, chapter_title, genre, model, folder): ai_isms = "', '".join(guidelines['ai_isms']) fw_examples = ", ".join([f"'He {w}'" for w in guidelines['filter_words'][:5]]) + # Calculate dynamic suggestion count based on length + 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""" - Act as a World-Class Literary Editor (e.g., Maxwell Perkins). Analyze this chapter draft with extreme scrutiny. - CHAPTER TITLE: {chapter_title} - GENRE: {genre} + ROLE: Senior Literary Editor + TASK: Critique chapter draft. - STRICT PROHIBITIONS (Automatic deduction): - - "AI-isms": '{ai_isms}'. (Evaluate in context of {genre}. Allow genre-appropriate tropes, but penalize robotic clichés). - - Filter Words: {fw_examples}, etc. (Show the sensation/action, don't state the internal process). - - 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). + METADATA: + - TITLE: {chapter_title} + - GENRE: {genre} - CRITERIA: + 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? @@ -399,13 +416,25 @@ def evaluate_chapter_quality(text, chapter_title, genre, model, folder): 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? Is the rhythm pleasing? Avoids purple prose and excessive adjectives. + 13. CLARITY & READABILITY: Is the text easy to follow? Are sentences clear and concise? - Rate on a scale of 1-10. (Be harsh. 10 is Pulitzer level. 6 is average. Anything below 8 needs work). + 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. - 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. "Expand the middle dialogue", "Add sensory details about the rain", "Dramatize the argument instead of summarizing it").' + 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: @@ -431,29 +460,21 @@ def check_pacing(bp, summary, last_chapter_text, last_chapter_data, remaining_ch 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']}". + ROLE: Structural Editor + TASK: Analyze pacing. - STORY SO FAR (Summary): - {summary[-3000:]} + 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)} - JUST WRITTEN (Last 2000 chars): - {last_chapter_text[-2000:]} + LOGIC: + - IF skipped major beats -> ADD_BRIDGE + - IF covered next chapter's beats -> CUT_NEXT + - ELSE -> OK - 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: + OUTPUT_FORMAT (JSON): {{ "status": "ok" or "add_bridge" or "cut_next", "reason": "Explanation...", @@ -474,17 +495,16 @@ def create_initial_persona(bp, folder): style = meta.get('style', {}) prompt = f""" - Create a fictional 'Author Persona' best suited to write this book. + ROLE: Creative Director + TASK: Create a fictional 'Author Persona'. - BOOK DETAILS: - Title: {meta.get('title')} - Genre: {meta.get('genre')} - Tone: {style.get('tone')} - Target Audience: {meta.get('target_audience')} + METADATA: + - TITLE: {meta.get('title')} + - GENRE: {meta.get('genre')} + - TONE: {style.get('tone')} + - AUDIENCE: {meta.get('target_audience')} - TASK: - Create a profile for the ideal writer of this book. - Return JSON: {{ "name": "Pen Name", "bio": "Description of writing style (voice, sentence structure, vocabulary)...", "age": "...", "gender": "..." }} + OUTPUT_FORMAT (JSON): {{ "name": "Pen Name", "bio": "Description of writing style (voice, sentence structure, vocabulary)...", "age": "...", "gender": "..." }} """ try: response = ai.model_logic.generate_content(prompt) @@ -500,20 +520,16 @@ def refine_persona(bp, text, folder): current_bio = ad.get('bio', 'Standard style.') prompt = f""" - Act as a Literary Stylist. Analyze this text sample from the book. + ROLE: Literary Stylist + TASK: Refine Author Bio based on text sample. - TEXT: - {text[:3000]} + INPUT_DATA: + - TEXT_SAMPLE: {text[:3000]} + - CURRENT_BIO: {current_bio} - CURRENT AUTHOR BIO: - {current_bio} + GOAL: Ensure future chapters sound exactly like the sample. Highlight quirks, patterns, vocabulary. - TASK: - Refine the Author Bio to better match the actual text produced. - Highlight specific stylistic quirks, sentence patterns, or vocabulary choices found in the text. - The goal is to ensure future chapters sound exactly like this one. - - Return JSON: {{ "bio": "Updated bio..." }} + OUTPUT_FORMAT (JSON): {{ "bio": "Updated bio..." }} """ try: response = ai.model_logic.generate_content(prompt) @@ -572,8 +588,9 @@ def write_chapter(chap, bp, folder, prev_sum, tracking=None, prev_content=None): for name, data in tracking['characters'].items(): desc = ", ".join(data.get('descriptors', [])) likes = ", ".join(data.get('likes_dislikes', [])) + speech = data.get('speech_style', 'Unknown') worn = data.get('last_worn', 'Unknown') - char_visuals += f"- {name}: {desc}\n * Likes/Dislikes: {likes}\n" + char_visuals += f"- {name}: {desc}\n * Speech: {speech}\n * Likes/Dislikes: {likes}\n" major = data.get('major_events', []) if major: char_visuals += f" * Major Events: {'; '.join(major)}\n" @@ -594,45 +611,69 @@ def write_chapter(chap, bp, folder, prev_sum, tracking=None, prev_content=None): prev_context_block = f"\nPREVIOUS CHAPTER TEXT (For Tone & Continuity):\n{trunc_content}\n" prompt = f""" - Write Chapter {chap['chapter_number']}: {chap['title']} - GENRE: {genre} + ROLE: Fiction Writer + TASK: Write Chapter {chap['chapter_number']}: {chap['title']} - PACING GUIDE: - - Format: {ls.get('label', 'Story')} - - Chapter Pacing: {pacing} - - Target Word Count: ~{est_words} (Use this as a guide, but prioritize story flow. Allow flexibility.) - - POV Character: {pov_char if pov_char else 'Protagonist'} + METADATA: + - GENRE: {genre} + - FORMAT: {ls.get('label', 'Story')} + - PACING: {pacing} + - TARGET_WORDS: ~{est_words} + - POV: {pov_char if pov_char else 'Protagonist'} - STYLE & FORMATTING: + STYLE_GUIDE: {style_block} - AUTHOR VOICE (CRITICAL): + AUTHOR_VOICE: {persona_info} - INSTRUCTION: - Write the scene. + INSTRUCTIONS: - Start with the Chapter Header formatted as Markdown H1 (e.g. '# Chapter X: Title'). Follow the 'Formatting Rules' for the header style. - - DEEP POV: Immerse the reader in the POV character's immediate experience. Filter descriptions through their specific worldview and emotional state. + - SENSORY ANCHORING: Start scenes by establishing Who, Where, and When immediately, anchored with a sensory detail. + - DEEP POV: Immerse the reader in the POV character's immediate experience. Filter descriptions through their specific worldview and emotional state. - SHOW, DON'T TELL: Focus on immediate action and internal reaction. Don't summarize feelings; show the physical manifestation of them. + - CAUSALITY: Ensure events follow a "Because of X, Y happened" logic, not just "And then X, and then Y". + - STAGING: When characters enter, describe their entrance. Don't let them just "appear" in dialogue. - SENSORY DETAILS: Use specific, grounding sensory details (smell, touch, sound) rather than generic descriptions. + - ACTIVE VOICE: Use active voice. Subject -> Verb -> Object. Avoid "was/were" constructions. + - STRONG VERBS: Delete adverbs. Use specific verbs (e.g. "trudged" instead of "walked slowly"). + - NO INFO-DUMPS: Weave backstory into dialogue or action. Do not stop the story to explain history. - AVOID CLICHÉS: Avoid common AI tropes (e.g., 'shiver down spine', 'palpable tension', 'unspoken agreement', 'testament to', 'tapestry of', 'azure', 'cerulean'). - MAINTAIN CONTINUITY: Pay close attention to the PREVIOUS CONTEXT. Characters must NOT know things that haven't happened yet or haven't been revealed to them. - CHARACTER INTERACTIONS: If characters are meeting for the first time in the summary, treat them as strangers. - SENTENCE VARIETY: Avoid repetitive sentence structures (e.g. starting multiple sentences with "He" or "She"). Vary sentence length to create rhythm. + - GENRE CONSISTENCY: Ensure all introductions of characters, places, items, or actions are strictly appropriate for the {genre} genre. Avoid anachronisms or tonal clashes. + + QUALITY_CRITERIA: + 1. ENGAGEMENT & TENSION: Grip the reader. Ensure conflict/tension in every scene. + 2. SCENE EXECUTION: Flesh out the middle. Avoid summarizing key moments. + 3. VOICE & TONE: Distinct narrative voice matching the genre. + 4. SENSORY IMMERSION: Engage all five senses. + 5. SHOW, DON'T TELL: Show emotions through physical reactions and subtext. + 6. CHARACTER AGENCY: Characters must drive the plot through active choices. + 7. PACING: Avoid rushing. Ensure the ending lands with impact. + 8. GENRE APPROPRIATENESS: Introductions of characters, places, items, or actions must be consistent with {genre} conventions. + 9. DIALOGUE AUTHENTICITY: Characters must sound distinct. Use subtext. Avoid "on-the-nose" dialogue. + 10. PLOT RELEVANCE: Every scene must advance the plot or character arcs. No filler. + 11. STAGING & FLOW: Characters must enter/exit physically. Paragraphs must transition logically. + 12. PROSE DYNAMICS: Vary sentence length. Use strong verbs. Avoid passive voice. + 13. CLARITY: Ensure sentences are clear and readable. Avoid convoluted phrasing. + - 'Very Fast': Rapid fire, pure action/dialogue, minimal description. - 'Fast': Punchy, keep it moving. - 'Standard': Balanced dialogue and description. - 'Slow': Detailed, atmospheric, immersive. - 'Very Slow': Deep introspection, heavy sensory detail, slow burn. - PREVIOUS CONTEXT (Story So Far): {prev_sum} + CONTEXT: + - STORY_SO_FAR: {prev_sum} {prev_context_block} - CHARACTERS: {json.dumps(bp['characters'])} + - CHARACTERS: {json.dumps(bp['characters'])} {char_visuals} - SCENE BEATS: {json.dumps(chap['beats'])} + - SCENE_BEATS: {json.dumps(chap['beats'])} - Output Markdown. + OUTPUT: Markdown text. """ current_text = "" try: @@ -647,6 +688,7 @@ def write_chapter(chap, bp, folder, prev_sum, tracking=None, prev_content=None): max_attempts = 5 SCORE_AUTO_ACCEPT = 9 SCORE_PASSING = 7 + SCORE_REWRITE_THRESHOLD = 6 best_score = 0 best_text = current_text @@ -681,6 +723,26 @@ def write_chapter(chap, bp, folder, prev_sum, tracking=None, prev_content=None): utils.log("WRITER", f" ⚠️ Quality low ({best_score}/{SCORE_PASSING}) but max attempts reached. Proceeding.") return best_text + if score < SCORE_REWRITE_THRESHOLD: + utils.log("WRITER", f" -> Score {score} < {SCORE_REWRITE_THRESHOLD}. Triggering FULL REWRITE (Fresh Draft)...") + + full_rewrite_prompt = prompt + f""" + + [SYSTEM ALERT: QUALITY CHECK FAILED] + The previous draft was rejected. + CRITIQUE: {critique} + + NEW TASK: Discard the previous attempt. Write a FRESH version of the chapter that addresses the critique above. + """ + + try: + resp_rewrite = ai.model_writer.generate_content(full_rewrite_prompt) + utils.log_usage(folder, "writer-flash", resp_rewrite.usage_metadata) + current_text = resp_rewrite.text + continue + except Exception as e: + utils.log("WRITER", f"Full rewrite failed: {e}. Falling back to refinement.") + utils.log("WRITER", f" -> Refining Ch {chap['chapter_number']} based on feedback...") guidelines = get_style_guidelines() @@ -690,32 +752,39 @@ def write_chapter(chap, bp, folder, prev_sum, tracking=None, prev_content=None): history_str = "\n".join(past_critiques[:-1]) if len(past_critiques) > 1 else "None" refine_prompt = f""" - Act as a Senior Editor. Rewrite this chapter to fix the issues identified below and ELEVATE the writing quality. + ROLE: Automated Editor + TASK: Rewrite text to satisfy critique and style rules. - CRITIQUE TO ADDRESS (MANDATORY): + CRITIQUE: {critique} - PREVIOUS CRITIQUES (Reference): + HISTORY: {history_str} - STYLIZED REWRITE INSTRUCTIONS: - 1. REMOVE FILTER WORDS: Delete "{fw_list}". Describe the image, sensation, or sound directly. - 2. VARY SENTENCE STRUCTURE: Do not start consecutive sentences with "He", "She", or "The". Use introductory clauses and varying lengths. - 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. - 7. STRONG VERBS: Avoid "was/were" constructions. Use active, specific verbs to drive the prose. - 8. EMOTIONAL RESONANCE: Ensure the POV character's internal state is clear and drives the narrative. + CONSTRAINTS: + {persona_info} + {style_block} + {char_visuals} + - BEATS: {json.dumps(chap.get('beats', []))} - STORY SO FAR: - {prev_sum} - {prev_context_block} + OPTIMIZATION_RULES: + 1. NO_FILTERS: Remove [{fw_list}]. + 2. VARIETY: No consecutive sentence starts. + 3. SUBTEXT: Indirect dialogue. + 4. TONE: Match {meta.get('genre', 'Fiction')}. + 5. INTERACTION: Use environment. + 6. DRAMA: No summary mode. + 7. ACTIVE_VERBS: No 'was/were' + ing. + 8. SHOWING: Physical emotion. + 9. LOGIC: Continuous staging. + 10. CLARITY: Simple structures. - CURRENT DRAFT: - {current_text} + INPUT_CONTEXT: + - SUMMARY: {prev_sum} + - PREVIOUS_TEXT: {prev_context_block} + - DRAFT: {current_text} - Return the polished, final version of the chapter in Markdown. + OUTPUT: Polished Markdown. """ try: # Use Logic model (Pro) for refinement to ensure higher quality prose @@ -733,11 +802,15 @@ def harvest_metadata(bp, folder, full_manuscript): full_text = "\n".join([c.get('content', '') for c in full_manuscript])[:500000] prompt = f""" - Analyze this manuscript text. - EXISTING CHARACTERS: {json.dumps(bp['characters'])} + ROLE: Data Extractor + TASK: Identify NEW significant characters. - TASK: Identify NEW significant characters that appear in the text but are missing from the list. - RETURN JSON: {{'new_characters': [{{'name':'...', 'role':'...', 'description':'...'}}]}} + INPUT_TEXT: + {full_text} + + KNOWN_CHARACTERS: {json.dumps(bp['characters'])} + + OUTPUT_FORMAT (JSON): {{ "new_characters": [{{ "name": "String", "role": "String", "description": "String" }}] }} """ try: response = ai.model_logic.generate_content(prompt) @@ -786,7 +859,12 @@ def update_persona_sample(bp, folder): if author_name not in personas: utils.log("SYSTEM", f"Generating new persona profile for '{author_name}'...") - prompt = f"Analyze this writing style (Tone, Voice, Vocabulary). Write a 1-sentence author bio describing it.\nTEXT: {sample_text[:1000]}" + prompt = f""" + ROLE: Literary Analyst + TASK: Analyze writing style (Tone, Voice, Vocabulary). + TEXT: {sample_text[:1000]} + OUTPUT: 1-sentence author bio. + """ try: response = ai.model_logic.generate_content(prompt) utils.log_usage(folder, "logic-pro", response.usage_metadata) @@ -810,14 +888,18 @@ def update_persona_sample(bp, folder): def refine_bible(bible, instruction, folder): utils.log("SYSTEM", f"Refining Bible with instruction: {instruction}") prompt = f""" - Act as a Senior Developmental Editor. - CURRENT JSON: {json.dumps(bible)} - USER INSTRUCTION: {instruction} + ROLE: Senior Developmental Editor + TASK: Update the Bible JSON based on instruction. - TASK: Update the JSON based on the instruction. Maintain valid JSON structure. - Ensure character motivations remain consistent and plot holes are avoided. + INPUT_DATA: + - CURRENT_JSON: {json.dumps(bible)} + - INSTRUCTION: {instruction} - RETURN ONLY THE JSON. + CONSTRAINTS: + - Maintain valid JSON structure. + - Ensure consistency. + + OUTPUT_FORMAT (JSON): The full updated Bible JSON object. """ try: response = ai.model_logic.generate_content(prompt) @@ -845,18 +927,15 @@ def analyze_consistency(bp, manuscript, folder): context = "\n".join(chapter_summaries) prompt = f""" - Act as a Continuity Editor. Analyze this book summary for plot holes and inconsistencies. + ROLE: Continuity Editor + TASK: Analyze book summary for plot holes. - CHARACTERS: {json.dumps(bp.get('characters', []))} - - CHAPTER SUMMARIES: + INPUT_DATA: + - CHARACTERS: {json.dumps(bp.get('characters', []))} + - SUMMARIES: {context} - TASK: - Identify 3-5 major continuity errors or plot holes (e.g. dead characters appearing, teleporting, forgotten injuries, motivation flips). - If none, say "No major issues found." - - Return JSON: {{ "issues": ["Issue 1", "Issue 2"], "score": 8, "summary": "Brief overall assessment." }} (Score 1-10 on logical consistency) + OUTPUT_FORMAT (JSON): {{ "issues": ["Issue 1", "Issue 2"], "score": 8, "summary": "Brief overall assessment." }} """ try: response = ai.model_logic.generate_content(prompt) @@ -888,56 +967,115 @@ def rewrite_chapter_content(bp, manuscript, chapter_num, instruction, folder): prev_text = prev_chap.get('content', '')[-3000:] # Last 3000 chars for context meta = bp.get('book_metadata', {}) + style = meta.get('style', {}) + + # Construct Persona Info (Maintain Voice) + 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" + + # Construct Character Visuals (Load tracking for consistency) + char_visuals = "" + 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 + + # Fix: Define fw_list for the prompt + guidelines = get_style_guidelines() + fw_list = '", "'.join(guidelines['filter_words']) prompt = f""" - Act as a Ghostwriter. Rewrite Chapter {chapter_num}: {target_chap.get('title', '')} + You are an expert fiction writing AI. Your task is to rewrite a specific chapter based on a user directive. - USER INSTRUCTION (PRIMARY 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} - STORY CONTEXT: - - Title: {meta.get('title')} - - Genre: {meta.get('genre')} - - Tone: {meta.get('style', {}).get('tone')} + 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. - PREVIOUS CHAPTER ENDING (Continuity): - {prev_text} + 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. - CURRENT DRAFT (Reference only - feel free to change significantly based on instruction): - {target_chap.get('content', '')[:5000]} - - CHARACTERS: - {json.dumps(bp.get('characters', []))} - - TASK: - Write the full chapter content in Markdown. - - Ensure it flows naturally from the previous chapter ending. - - Follow the User Instruction strictly, even if it contradicts the current draft. - - Maintain the established character voices. + 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.model_writer.generate_content(prompt) utils.log_usage(folder, "writer-flash", response.usage_metadata) - return response.text + try: + data = json.loads(utils.clean_json(response.text)) + return data.get('content'), data.get('summary') + except: + # Fallback if model returns raw text instead of JSON + return response.text, None except Exception as e: utils.log("WRITER", f"Rewrite failed: {e}") - return None + return None, None -def check_and_propagate(bp, manuscript, changed_chap_num, folder): +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}...") # Find the changed chapter changed_chap = next((c for c in manuscript if c['num'] == changed_chap_num), None) if not changed_chap: return None - # Summarize the change to save tokens - change_summary_prompt = f"Summarize the key events and ending state of this chapter:\n{changed_chap.get('content', '')[:10000]}" - try: - resp = ai.model_writer.generate_content(change_summary_prompt) - current_context = resp.text - except: - current_context = changed_chap.get('content', '')[-2000:] # Fallback + if change_summary: + current_context = change_summary + else: + # Summarize the change to save tokens (Fallback if no summary provided) + 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.model_writer.generate_content(change_summary_prompt) + current_context = resp.text + except: + current_context = changed_chap.get('content', '')[-2000:] # Fallback original_change_context = current_context # Iterate subsequent chapters @@ -979,20 +1117,18 @@ def check_and_propagate(bp, manuscript, changed_chap_num, folder): chapter_summaries.append(f"Ch {rc['num']}: {excerpt}") scan_prompt = f""" - We are propagating a change from Chapter {changed_chap_num}. - The immediate ripple effect seems to have stopped. + ROLE: Continuity Scanner + TASK: Identify chapters impacted by a change. - ORIGINAL CHANGE CONTEXT: + CHANGE_CONTEXT: {original_change_context} - REMAINING CHAPTERS: + CHAPTER_SUMMARIES: {json.dumps(chapter_summaries)} - TASK: - Identify any later chapters that mention items, characters, or locations involved in the Change Context. - Return a JSON list of Chapter Numbers (integers) that might need updating. - Example: [5, 12] - If none, return []. + CRITERIA: Identify later chapters that mention items, characters, or locations involved in the Change Context. + + OUTPUT_FORMAT (JSON): [Chapter_Number_Int, ...] """ try: @@ -1020,40 +1156,50 @@ def check_and_propagate(bp, manuscript, changed_chap_num, folder): utils.log("WRITER", f" -> Checking Ch {target_chap['num']} for continuity...") prompt = f""" - Chapter {changed_chap_num} was just rewritten. - NEW CONTEXT/ENDING of previous section: - {current_context} + ROLE: Continuity Checker + TASK: Determine if chapter needs rewrite based on new context. - CURRENT TEXT of Ch {target_chap['num']}: - {target_chap['content'][:5000]}... (truncated) + INPUT_DATA: + - CHANGED_CHAPTER: {changed_chap_num} + - NEW_CONTEXT: {current_context} + - CURRENT_CHAPTER_TEXT: {target_chap['content'][:5000]}... - TASK: - Does Ch {target_chap['num']} need to be rewritten to maintain continuity with the new context? - - If YES (e.g. references old events that changed, character states don't match): Rewrite the chapter fully in Markdown. - - If NO (it fits fine): Return ONLY the string "NO_CHANGE". + DECISION_LOGIC: + - Compare CURRENT_CHAPTER_TEXT with NEW_CONTEXT. + - If the chapter contradicts the new context (e.g. references events that didn't happen, or characters who are now dead/absent), it needs a REWRITE. + - If it fits fine, NO_CHANGE. + + OUTPUT_FORMAT (JSON): + {{ + "status": "NO_CHANGE" or "REWRITE", + "reason": "Brief explanation", + "content": "Full Markdown text of the rewritten chapter (ONLY if status is REWRITE, otherwise null)" + }} """ try: response = ai.model_writer.generate_content(prompt) - text = response.text.strip() + data = json.loads(utils.clean_json(response.text)) - if "NO_CHANGE" in text[:20] and len(text) < 100: + if data.get('status') == 'NO_CHANGE': utils.log("WRITER", f" -> Ch {target_chap['num']} is consistent.") # Update context for next iteration using existing text current_context = f"Ch {target_chap['num']} Summary: " + target_chap.get('content', '')[-2000:] consecutive_no_changes += 1 - else: - utils.log("WRITER", f" -> Rewriting Ch {target_chap['num']} to fix continuity.") - target_chap['content'] = text - changes_made = True - # Update context with NEW text - current_context = f"Ch {target_chap['num']} Summary: " + text[-2000:] - consecutive_no_changes = 0 - - # Save immediately to prevent data loss if subsequent checks fail - try: - with open(os.path.join(folder, "manuscript.json"), 'w') as f: json.dump(manuscript, f, indent=2) - except: pass + 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 + # Update context with NEW text + current_context = f"Ch {target_chap['num']} Summary: " + new_text[-2000:] + consecutive_no_changes = 0 + + # Save immediately to prevent data loss if subsequent checks fail + 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}") diff --git a/modules/web_app.py b/modules/web_app.py index f1256a7..cc9f02e 100644 --- a/modules/web_app.py +++ b/modules/web_app.py @@ -162,32 +162,36 @@ def project_setup_wizard(): return redirect(url_for('index')) prompt = f""" - Analyze this story concept and suggest metadata for a book or series. + ROLE: Publishing Analyst + TASK: Suggest metadata for a story concept. + 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 ["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 - - author_bio: string (suggested persona bio) + OUTPUT_FORMAT (JSON): + {{ + "title": "String", + "genre": "String", + "target_audience": "String", + "tone": "String", + "length_category": "String (Select code: '01'=Chapter Book, '1'=Flash Fiction, '2'=Short Story, '2b'=Young Adult, '3'=Novella, '4'=Novel, '5'=Epic)", + "estimated_chapters": Int, + "estimated_word_count": "String (e.g. '75,000')", + "include_prologue": Bool, + "include_epilogue": Bool, + "tropes": ["String"], + "pov_style": "String", + "time_period": "String", + "spice": "String", + "violence": "String", + "is_series": Bool, + "series_title": "String", + "narrative_tense": "String", + "language_style": "String", + "dialogue_style": "String", + "page_orientation": "Portrait|Landscape|Square", + "formatting_rules": ["String (e.g. 'Chapter Headers: Number + Title')"], + "author_bio": "String" + }} """ suggestions = {} @@ -227,12 +231,15 @@ def project_setup_refine(): except: pass prompt = f""" - Update these project suggestions based on the user instruction. - ORIGINAL CONCEPT: {concept} - CURRENT TITLE: {current_state['title']} - INSTRUCTION: {instruction} + ROLE: Publishing Analyst + TASK: Refine project metadata based on user instruction. - RETURN JSON with the same keys as a full analysis (title, genre, length_category, etc). + INPUT_DATA: + - ORIGINAL_CONCEPT: {concept} + - CURRENT_TITLE: {current_state['title']} + - INSTRUCTION: {instruction} + + OUTPUT_FORMAT (JSON): Same structure as the initial analysis (title, genre, length_category, etc). Ensure length_category matches the word count. """ suggestions = {} @@ -1393,19 +1400,20 @@ def analyze_persona(): sample = data.get('sample_text', '') prompt = f""" - Act as a Literary Analyst. Create or analyze an Author Persona profile. + ROLE: Literary Analyst + TASK: Create or analyze an Author Persona profile. - INPUT DATA: - Name: {data.get('name')} - Age: {data.get('age')} | Gender: {data.get('gender')} | Nationality: {data.get('nationality')} - Sample Text: {sample[:3000]} + INPUT_DATA: + - NAME: {data.get('name')} + - DEMOGRAPHICS: Age: {data.get('age')} | Gender: {data.get('gender')} | Nationality: {data.get('nationality')} + - SAMPLE_TEXT: {sample[:3000]} - TASK: - 1. 'bio': Write a 2-3 sentence description of the writing style. If sample is provided, analyze it. If not, invent a style that fits the demographics/name. - 2. 'voice_keywords': Comma-separated list of 3-5 adjectives describing the voice (e.g. Gritty, Whimsical, Sarcastic). - 3. 'style_inspirations': Comma-separated list of 1-3 famous authors or genres that this style resembles. + INSTRUCTIONS: + 1. BIO: Write a 2-3 sentence description of the writing style. If sample is provided, analyze it. If not, invent a style that fits the demographics/name. + 2. KEYWORDS: Comma-separated list of 3-5 adjectives describing the voice (e.g. Gritty, Whimsical, Sarcastic). + 3. INSPIRATIONS: Comma-separated list of 1-3 famous authors or genres that this style resembles. - RETURN JSON: {{ "bio": "...", "voice_keywords": "...", "style_inspirations": "..." }} + OUTPUT_FORMAT (JSON): {{ "bio": "String", "voice_keywords": "String", "style_inspirations": "String" }} """ try: response = ai.model_logic.generate_content(prompt) diff --git a/modules/web_tasks.py b/modules/web_tasks.py index d70d7be..98c31b2 100644 --- a/modules/web_tasks.py +++ b/modules/web_tasks.py @@ -350,9 +350,10 @@ def rewrite_chapter_task(run_id, project_path, book_folder, chap_num, instructio ai.init_models() - new_text = story.rewrite_chapter_content(bp, ms, chap_num, instruction, book_path) + result = story.rewrite_chapter_content(bp, ms, chap_num, instruction, book_path) - if new_text: + if result and result[0]: + new_text, summary = result for ch in ms: if ch.get('num') == chap_num: ch['content'] = new_text @@ -361,7 +362,7 @@ def rewrite_chapter_task(run_id, project_path, book_folder, chap_num, instructio # Save the primary rewrite immediately with open(ms_path, 'w') as f: json.dump(ms, f, indent=2) - updated_ms = story.check_and_propagate(bp, ms, chap_num, book_path) + updated_ms = story.check_and_propagate(bp, ms, chap_num, book_path, change_summary=summary) if updated_ms: ms = updated_ms diff --git a/wizard.py b/wizard.py index a595ab6..5d1fd10 100644 --- a/wizard.py +++ b/wizard.py @@ -183,31 +183,35 @@ class BookWizard: 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. + ROLE: Publishing Analyst + TASK: Suggest metadata for a story concept. + 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 + OUTPUT_FORMAT (JSON): + {{ + "title": "String", + "genre": "String", + "target_audience": "String", + "tone": "String", + "length_category": "String (Select code: '01'=Chapter Book, '1'=Flash Fiction, '2'=Short Story, '2b'=Young Adult, '3'=Novella, '4'=Novel, '5'=Epic)", + "estimated_chapters": Int, + "estimated_word_count": "String (e.g. '75,000')", + "include_prologue": Bool, + "include_epilogue": Bool, + "tropes": ["String"], + "pov_style": "String", + "time_period": "String", + "spice": "String", + "violence": "String", + "is_series": Bool, + "series_title": "String", + "narrative_tense": "String", + "language_style": "String", + "dialogue_style": "String", + "page_orientation": "Portrait|Landscape|Square", + "formatting_rules": ["String (e.g. 'Chapter Headers: Number + Title')"] + }} """ suggestions = self.ask_gemini_json(prompt) @@ -256,10 +260,14 @@ class BookWizard: 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. + ROLE: Publishing Analyst + TASK: Refine project metadata based on user instruction. + + INPUT_DATA: + - CURRENT_JSON: {json.dumps(suggestions)} + - INSTRUCTION: {instruction} + + OUTPUT_FORMAT (JSON): Same structure as input. Ensure length_category matches word count. """ new_sugg = self.ask_gemini_json(refine_prompt) if new_sugg: suggestions = new_sugg @@ -496,25 +504,22 @@ class BookWizard: 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. + ROLE: Creative Director + TASK: Create a comprehensive Book Bible. - PROJECT METADATA: {json.dumps(self.data['project_metadata'])} - EXISTING BOOKS STRUCTURE: {json.dumps(self.data['books'])} + INPUT_DATA: + - METADATA: {json.dumps(self.data['project_metadata'])} + - BOOKS: {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). + INSTRUCTIONS: + 1. Create Main Characters. + 2. For EACH book: Generate Title, Plot Summary (manual_instruction), and 10-15 Plot Beats. - RETURN JSON in standard Bible format: + OUTPUT_FORMAT (JSON): {{ - "characters": [ {{ "name": "...", "role": "...", "description": "..." }} ], + "characters": [ {{ "name": "String", "role": "String", "description": "String" }} ], "books": [ - {{ "book_number": 1, "title": "...", "manual_instruction": "...", "plot_beats": ["...", "..."] }}, - ... + {{ "book_number": Int, "title": "String", "manual_instruction": "String", "plot_beats": ["String"] }} ] }} """ @@ -646,12 +651,14 @@ class BookWizard: 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} + ROLE: Senior Editor + TASK: Update the Bible JSON based on instruction. - TASK: Update the JSON based on the instruction. Maintain valid JSON structure. - RETURN ONLY THE JSON. + INPUT_DATA: + - CURRENT_JSON: {json.dumps(current_data)} + - INSTRUCTION: {instruction} + + OUTPUT_FORMAT (JSON): The full updated JSON object. """ new_data = self.ask_gemini_json(prompt)