v1.3.0: Improve all AI prompts, refinement loops, and cover generation accuracy

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 <noreply@anthropic.com>
This commit is contained in:
2026-02-20 10:38:36 -05:00
parent 2a9a605800
commit 1964c9c2a5
3 changed files with 179 additions and 89 deletions

View File

@@ -65,4 +65,4 @@ LENGTH_DEFINITIONS = {
}
# --- SYSTEM ---
VERSION = "1.2.0"
VERSION = "1.3.0"

View File

@@ -90,18 +90,40 @@ 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).
TASK: Write a compelling back-cover blurb for a {meta.get('genre', 'fiction')} novel.
INPUT_DATA:
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', []))}
- AUDIENCE: {meta.get('target_audience', 'General')}
- PROTAGONIST: {protagonist_desc}
- LOGLINE: {bp.get('manual_instruction', '(none)')}
- KEY PLOT BEATS:
{beats_text}
OUTPUT: Text only.
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.
TASK: Design a professional book cover for an AI image generator.
METADATA:
BOOK:
- TITLE: {meta.get('title')}
- GENRE: {meta.get('genre')}
- TONE: {meta.get('style', {}).get('tone', 'Balanced')}
- GENRE: {genre}
- TONE: {tone}
- SUGGESTED_VISUAL_STYLE: {suggested_style}
VISUAL_CONTEXT:
{visual_context}
VISUAL_CONTEXT (characters and key themes from the story):
{visual_context if visual_context else "Use genre conventions."}
USER_FEEDBACK:
{f"{feedback}" if feedback else "None"}
USER_FEEDBACK: {feedback if feedback else "None"}
DESIGN_INSTRUCTION: {design_instruction if design_instruction else "Create a compelling, genre-appropriate cover."}
INSTRUCTION:
{f"{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):
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}")

View File

@@ -223,13 +223,31 @@ 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.
TASK: Create a detailed structural event outline for a {target_chapters}-chapter book.
ARCHETYPE: {structure_type}
TITLE: {bp['book_metadata']['title']}
EXISTING_BEATS: {json.dumps(beats_context)}
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" }}] }}
"""
@@ -612,6 +630,7 @@ 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']}
@@ -619,10 +638,18 @@ def write_chapter(chap, bp, folder, prev_sum, tracking=None, prev_content=None):
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.
TASK: Rewrite the draft chapter to address the critique. Preserve the narrative content and approximate word count.
CRITIQUE:
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.
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.
INPUT_CONTEXT:
- SUMMARY: {prev_sum}
- PREVIOUS_TEXT: {prev_context_block}
- DRAFT: {current_text}
DRAFT_TO_REWRITE:
{current_text}
OUTPUT: Polished Markdown.
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.
TASK: Determine if a chapter contradicts a story change. If it does, rewrite it to fix the contradiction.
INPUT_DATA:
- CHANGED_CHAPTER: {changed_chap_num}
- NEW_CONTEXT: {current_context}
- CURRENT_CHAPTER_TEXT: {target_chap['content'][:5000]}...
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)"
}}
"""