Strengthened writing.

This commit is contained in:
2026-02-05 22:26:55 -05:00
parent e6110a6a54
commit 7e5dbe6f00
7 changed files with 577 additions and 350 deletions

38
main.py
View File

@@ -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]"

View File

@@ -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)

View File

@@ -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

View File

@@ -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
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
# 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}")

View File

@@ -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)

View File

@@ -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

View File

@@ -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)