From 1964c9c2a5ef6a800c8273445ac7197ef765b7bd Mon Sep 17 00:00:00 2001 From: Mike Wichers Date: Fri, 20 Feb 2026 10:38:36 -0500 Subject: [PATCH] v1.3.0: Improve all AI prompts, refinement loops, and cover generation accuracy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit story.py — write_chapter(): - Added POSITION context ("Chapter N of Total") so the AI calibrates narrative tension correctly (setup vs escalation vs climax/payoff) - Moved PACING_GUIDE to sit directly after PACING metadata instead of being buried after 13 quality criteria items where the AI rarely reads it - Removed duplicate pacing descriptions that appeared after QUALITY_CRITERIA story.py — refinement loop: - Capped critique history to last 2 entries (was accumulating all previous attempts, wasting tokens and confusing the model on attempt 4-5) - Added TARGET_WORDS and BEATS constraints to the refinement prompt to prevent chapters from shrinking or losing plot beats during editing passes - Restructured refinement prompt with explicit HARD_CONSTRAINTS section story.py — check_and_propagate(): - Increased chapter context from 5000 to 12000 chars for continuity rewrites (was asking for a full chapter rewrite but only providing a fragment) - Added explicit word count target to rewrite so chapters are not truncated - Added conservative decision bias: only rewrite on genuine contradictions story.py — plan_structure(): - Now passes TARGET_CHAPTERS, TARGET_WORDS, GENRE, and CHARACTERS to the structure AI — it was planning blindly without knowing the book's scale marketing.py — generate_blurb(): - Rewrote prompt with 4-part structure: Hook → Stakes → Tension → Close - Formats plot beats as a readable list instead of raw JSON array - Extracts protagonist automatically for personalised blurb copy - Added genre-tone matching, present-tense voice, and no-spoiler rule marketing.py — generate_cover(): - Added genre-to-visual-style mapping (thriller → cinematic, fantasy → epic digital painting, romance → painterly, etc.) - Art prompt instructions now enforce: no text/letters/watermarks, rule-of-thirds composition, explicit focal point, lighting description, colour palette - Replaced generic image evaluation with a 5-criteria book-cover rubric: visual impact, genre fit, composition, quality, and clean image (no text) - Score penalties: -3 for visible text/watermarks, -2 for blur/deformed anatomy Co-Authored-By: Claude Sonnet 4.6 --- config.py | 2 +- modules/marketing.py | 114 +++++++++++++++++++++++--------- modules/story.py | 152 ++++++++++++++++++++++++++----------------- 3 files changed, 179 insertions(+), 89 deletions(-) diff --git a/config.py b/config.py index fab85ad..a13d2b7 100644 --- a/config.py +++ b/config.py @@ -65,4 +65,4 @@ LENGTH_DEFINITIONS = { } # --- SYSTEM --- -VERSION = "1.2.0" \ No newline at end of file +VERSION = "1.3.0" \ No newline at end of file diff --git a/modules/marketing.py b/modules/marketing.py index 0f3ae1d..cbbe71a 100644 --- a/modules/marketing.py +++ b/modules/marketing.py @@ -89,19 +89,41 @@ def evaluate_image_quality(image_path, prompt, model, folder=None): def generate_blurb(bp, folder): utils.log("MARKETING", "Generating blurb...") meta = bp.get('book_metadata', {}) - + + # Format beats as a readable list, not raw JSON + beats = bp.get('plot_beats', []) + beats_text = "\n".join(f" - {b}" for b in beats[:6]) if beats else " - (no beats provided)" + + # Format protagonist for the blurb + chars = bp.get('characters', []) + protagonist = next((c for c in chars if 'protagonist' in c.get('role', '').lower()), None) + protagonist_desc = f"{protagonist['name']} — {protagonist.get('description', '')}" if protagonist else "the protagonist" + prompt = f""" ROLE: Marketing Copywriter - TASK: Write a back-cover blurb (150-200 words). - - INPUT_DATA: + TASK: Write a compelling back-cover blurb for a {meta.get('genre', 'fiction')} novel. + + BOOK DETAILS: - 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. + - AUDIENCE: {meta.get('target_audience', 'General')} + - PROTAGONIST: {protagonist_desc} + - LOGLINE: {bp.get('manual_instruction', '(none)')} + - KEY PLOT BEATS: +{beats_text} + + BLURB STRUCTURE: + 1. HOOK (1-2 sentences): Open with the protagonist's world and the inciting disruption. Make it urgent. + 2. STAKES (2-3 sentences): Raise the central conflict. What does the protagonist stand to lose? + 3. TENSION (1-2 sentences): Hint at the impossible choice or escalating danger without revealing the resolution. + 4. HOOK CLOSE (1 sentence): End with a tantalising question or statement that demands the reader open the book. + + RULES: + - 150-200 words total. + - DO NOT reveal the ending or resolution. + - Match the genre's marketing tone ({meta.get('genre', 'fiction')}: e.g. thriller = urgent/terse, romance = emotionally charged, fantasy = epic/wondrous, horror = dread-laden). + - Use present tense for the blurb voice. + - No "Blurb:", no title prefix, no labels — marketing copy only. """ try: response = ai.model_writer.generate_content(prompt) @@ -167,30 +189,51 @@ def generate_cover(bp, folder, tracking=None, feedback=None, interactive=False): except: utils.log("MARKETING", "Feedback analysis failed. Defaulting to full regeneration.") + genre = meta.get('genre', 'Fiction') + tone = meta.get('style', {}).get('tone', 'Balanced') + # Map genre to visual style suggestion + genre_style_map = { + 'thriller': 'dark, cinematic, high-contrast photography style', + 'mystery': 'moody, atmospheric, noir-inspired painting', + 'romance': 'warm, painterly, soft-focus illustration', + 'fantasy': 'epic digital painting, rich colours, mythic scale', + 'science fiction': 'sharp digital art, cool palette, futuristic', + 'horror': 'unsettling, dark atmospheric painting, desaturated', + 'historical fiction': 'classical oil painting style, period-accurate', + 'young adult': 'vibrant illustrated style, bold colours', + } + suggested_style = genre_style_map.get(genre.lower(), 'professional digital illustration or photography') + design_prompt = f""" ROLE: Art Director - TASK: Design a book cover. - - METADATA: - - TITLE: {meta.get('title')} - - GENRE: {meta.get('genre')} - - TONE: {meta.get('style', {}).get('tone', 'Balanced')} + TASK: Design a professional book cover for an AI image generator. - VISUAL_CONTEXT: - {visual_context} - - 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): + BOOK: + - TITLE: {meta.get('title')} + - GENRE: {genre} + - TONE: {tone} + - SUGGESTED_VISUAL_STYLE: {suggested_style} + + VISUAL_CONTEXT (characters and key themes from the story): + {visual_context if visual_context else "Use genre conventions."} + + USER_FEEDBACK: {feedback if feedback else "None"} + DESIGN_INSTRUCTION: {design_instruction if design_instruction else "Create a compelling, genre-appropriate cover."} + + COVER_ART_RULES: + - The art_prompt must produce an image with NO text, no letters, no numbers, no watermarks, no UI elements, no logos. + - Describe a clear FOCAL POINT (e.g. the protagonist, a dramatic scene, a symbolic object). + - Use RULE OF THIRDS composition — leave visual space at top and/or bottom for the title and author text to be overlaid. + - Describe LIGHTING that reinforces the tone (e.g. "harsh neon backlight" for thriller, "golden hour" for romance). + - Describe the COLOUR PALETTE explicitly (e.g. "deep crimson and shadow-black", "soft rose gold and cream"). + - Characters must match their descriptions from VISUAL_CONTEXT if present. + + OUTPUT_FORMAT (JSON only, no markdown): {{ - "font_name": "Name of a popular Google Font (e.g. Roboto, Cinzel, Oswald, Playfair Display)", - "primary_color": "#HexCode (Background)", - "text_color": "#HexCode (Contrast)", - "art_prompt": "A detailed description of the cover art for an image generator. Explicitly describe characters based on the visual context." + "font_name": "Name of a Google Font suited to the genre (e.g. Cinzel for fantasy, Oswald for thriller, Playfair Display for romance)", + "primary_color": "#HexCode (dominant background/cover colour)", + "text_color": "#HexCode (high contrast against primary_color)", + "art_prompt": "Detailed {suggested_style} image generation prompt. Begin with the style. Describe composition, focal point, lighting, colour palette, and any characters. End with: No text, no letters, no watermarks, photorealistic/painted quality, 8k detail." }} """ try: @@ -243,7 +286,18 @@ def generate_cover(bp, folder, tracking=None, feedback=None, interactive=False): result.images[0].save(attempt_path) utils.log_usage(folder, "imagen", image_count=1) - score, critique = evaluate_image_quality(attempt_path, art_prompt, ai.model_writer, folder) + cover_eval_criteria = ( + f"Book cover art for a {genre} novel titled '{meta.get('title')}'.\n\n" + f"Evaluate STRICTLY as a professional book cover on these criteria:\n" + f"1. VISUAL IMPACT: Is the image immediately arresting and compelling?\n" + f"2. GENRE FIT: Does the visual style, mood, and palette match {genre}?\n" + f"3. COMPOSITION: Is there a clear focal point? Are top/bottom areas usable for title/author text?\n" + f"4. QUALITY: Is the image sharp, detailed, and free of deformities or blurring?\n" + f"5. CLEAN IMAGE: Are there absolutely NO text, watermarks, letters, or UI artifacts?\n" + f"Score 1-10. Deduct 3 points if any text/watermarks are visible. " + f"Deduct 2 if the image is blurry or has deformed anatomy." + ) + score, critique = evaluate_image_quality(attempt_path, cover_eval_criteria, ai.model_writer, folder) if score is None: score = 0 utils.log("MARKETING", f" -> Image Score: {score}/10. Critique: {critique}") diff --git a/modules/story.py b/modules/story.py index 668e1d1..74d66b0 100644 --- a/modules/story.py +++ b/modules/story.py @@ -223,14 +223,32 @@ def plan_structure(bp, folder): if not beats_context: 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 structural event outline. - - ARCHETYPE: {structure_type} - TITLE: {bp['book_metadata']['title']} - EXISTING_BEATS: {json.dumps(beats_context)} - + 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: @@ -612,16 +630,25 @@ def write_chapter(chap, bp, folder, prev_sum, tracking=None, prev_content=None): trunc_content = prev_content[-3000:] if len(prev_content) > 3000 else prev_content prev_context_block = f"\nPREVIOUS CHAPTER TEXT (For Tone & Continuity):\n{trunc_content}\n" + total_chapters = ls.get('chapters', '?') prompt = f""" ROLE: Fiction Writer TASK: Write Chapter {chap['chapter_number']}: {chap['title']} - + METADATA: - GENRE: {genre} - FORMAT: {ls.get('label', 'Story')} - - PACING: {pacing} - - TARGET_WORDS: ~{est_words} + - POSITION: Chapter {chap['chapter_number']} of {total_chapters} — calibrate narrative tension accordingly (early = setup/intrigue, middle = escalation, final third = payoff/climax) + - PACING: {pacing} — see PACING_GUIDE below + - TARGET_WORDS: ~{est_words} (write to this length; do not summarise to save space) - POV: {pov_char if pov_char else 'Protagonist'} + + PACING_GUIDE: + - 'Very Fast': Pure action/dialogue. Minimal description. Short punchy paragraphs. + - 'Fast': Keep momentum. No lingering. Cut to the next beat quickly. + - 'Standard': Balanced dialogue and description. Standard paragraph lengths. + - 'Slow': Detailed, atmospheric. Linger on emotion and environment. + - 'Very Slow': Deep introspection. Heavy sensory immersion. Slow burn tension. STYLE_GUIDE: {style_block} @@ -662,12 +689,6 @@ def write_chapter(chap, bp, folder, prev_sum, tracking=None, prev_content=None): 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. - CONTEXT: - STORY_SO_FAR: {prev_sum} {prev_context_block} @@ -750,43 +771,50 @@ def write_chapter(chap, bp, folder, prev_sum, tracking=None, prev_content=None): guidelines = get_style_guidelines() fw_list = '", "'.join(guidelines['filter_words']) - # Exclude current critique from history to avoid duplication in prompt - history_str = "\n".join(past_critiques[:-1]) if len(past_critiques) > 1 else "None" - + # Cap history to last 2 critiques to avoid token bloat + history_str = "\n".join(past_critiques[-3:-1]) if len(past_critiques) > 1 else "None" + refine_prompt = f""" ROLE: Automated Editor - TASK: Rewrite text to satisfy critique and style rules. - - CRITIQUE: + TASK: Rewrite the draft chapter to address the critique. Preserve the narrative content and approximate word count. + + CURRENT_CRITIQUE: {critique} - - HISTORY: + + PREVIOUS_ATTEMPTS (context only): {history_str} - - CONSTRAINTS: + + HARD_CONSTRAINTS: + - TARGET_WORDS: ~{est_words} (maintain this length — do not condense or summarise) + - BEATS MUST BE COVERED: {json.dumps(chap.get('beats', []))} + - SUMMARY CONTEXT: {prev_sum[:1500]} + + AUTHOR_VOICE: {persona_info} + + STYLE: {style_block} {char_visuals} - - BEATS: {json.dumps(chap.get('beats', []))} - - 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. - - INPUT_CONTEXT: - - SUMMARY: {prev_sum} - - PREVIOUS_TEXT: {prev_context_block} - - DRAFT: {current_text} - - OUTPUT: Polished Markdown. + + PROSE_RULES (fix each one found in the draft): + 1. FILTER_REMOVAL: Remove filter words [{fw_list}] — rewrite to show the sensation directly. + 2. VARIETY: No two consecutive sentences starting with the same word or pronoun. + 3. SUBTEXT: Dialogue must imply meaning — not state it outright. + 4. TONE: Match {meta.get('genre', 'Fiction')} conventions throughout. + 5. ENVIRONMENT: Characters interact with their physical space. + 6. NO_SUMMARY_MODE: Dramatise key moments — do not skip or summarise them. + 7. ACTIVE_VOICE: Replace 'was/were + verb-ing' constructions with active alternatives. + 8. SHOWING: Render emotion through physical reactions, not labels. + 9. STAGING: Characters must enter and exit physically — no teleporting. + 10. CLARITY: Prefer simple sentence structures over convoluted ones. + + DRAFT_TO_REWRITE: + {current_text} + + PREVIOUS_CHAPTER_ENDING (maintain continuity): + {prev_context_block} + + OUTPUT: Complete polished chapter in Markdown. Include the chapter header. Same approximate length as the draft. """ try: # Use Writer model (Flash) for refinement to save costs (Flash 1.5 is sufficient for editing) @@ -1159,25 +1187,33 @@ def check_and_propagate(bp, manuscript, changed_chap_num, folder, change_summary 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 chapter needs rewrite based on new context. - - INPUT_DATA: - - CHANGED_CHAPTER: {changed_chap_num} - - NEW_CONTEXT: {current_context} - - CURRENT_CHAPTER_TEXT: {target_chap['content'][:5000]}... - + 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: - - 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. - + - 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", - "content": "Full Markdown text of the rewritten chapter (ONLY if status is REWRITE, otherwise null)" + "reason": "Brief explanation of the contradiction or why it's consistent", + "content": "Full Markdown rewritten chapter (ONLY if status is REWRITE, otherwise null)" }} """