Strengthened writing.
This commit is contained in:
38
main.py
38
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...")
|
utils.log("RESUME", "Rebuilding 'Story So Far' from existing manuscript...")
|
||||||
try:
|
try:
|
||||||
combined_text = "\n".join([f"Chapter {c['num']}: {c['content']}" for c in ms])
|
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)
|
utils.log_usage(folder, "writer-flash", resp_sum.usage_metadata)
|
||||||
summary = resp_sum.text
|
summary = resp_sum.text
|
||||||
except: summary = "The story continues."
|
except: summary = "The story continues."
|
||||||
@@ -161,29 +168,30 @@ def process_book(bp, folder, context="", resume=False, interactive=False):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
update_prompt = f"""
|
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.
|
INPUT_DATA:
|
||||||
GOAL: Maintain a perfect memory of the plot for continuity.
|
- CURRENT_SUMMARY:
|
||||||
|
|
||||||
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:
|
|
||||||
{summary}
|
{summary}
|
||||||
|
- NEW_CHAPTER_TEXT:
|
||||||
NEW CHAPTER CONTENT:
|
|
||||||
{txt}
|
{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)
|
resp_sum = ai.model_writer.generate_content(update_prompt)
|
||||||
utils.log_usage(folder, "writer-flash", resp_sum.usage_metadata)
|
utils.log_usage(folder, "writer-flash", resp_sum.usage_metadata)
|
||||||
summary = resp_sum.text
|
summary = resp_sum.text
|
||||||
except:
|
except:
|
||||||
try:
|
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)
|
utils.log_usage(folder, "writer-flash", resp_fallback.usage_metadata)
|
||||||
summary += f"\n\nChapter {ch['chapter_number']}: " + resp_fallback.text
|
summary += f"\n\nChapter {ch['chapter_number']}: " + resp_fallback.text
|
||||||
except: summary += f"\n\nChapter {ch['chapter_number']}: [Content processed]"
|
except: summary += f"\n\nChapter {ch['chapter_number']}: [Content processed]"
|
||||||
|
|||||||
@@ -85,7 +85,30 @@ def select_best_models(force_refresh=False):
|
|||||||
utils.log("SYSTEM", f"Bootstrapping model selection with: {bootstrapper}")
|
utils.log("SYSTEM", f"Bootstrapping model selection with: {bootstrapper}")
|
||||||
|
|
||||||
model = genai.GenerativeModel(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:
|
try:
|
||||||
response = model.generate_content(prompt)
|
response = model.generate_content(prompt)
|
||||||
|
|||||||
@@ -74,7 +74,12 @@ def evaluate_image_quality(image_path, prompt, model, folder=None):
|
|||||||
if not HAS_PIL: return None, "PIL not installed"
|
if not HAS_PIL: return None, "PIL not installed"
|
||||||
try:
|
try:
|
||||||
img = Image.open(image_path)
|
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)
|
if folder: utils.log_usage(folder, "logic-pro", response.usage_metadata)
|
||||||
data = json.loads(utils.clean_json(response.text))
|
data = json.loads(utils.clean_json(response.text))
|
||||||
return data.get('score'), data.get('reason')
|
return data.get('score'), data.get('reason')
|
||||||
@@ -85,12 +90,17 @@ def generate_blurb(bp, folder):
|
|||||||
meta = bp.get('book_metadata', {})
|
meta = bp.get('book_metadata', {})
|
||||||
|
|
||||||
prompt = f"""
|
prompt = f"""
|
||||||
Write a compelling back-cover blurb (approx 150-200 words) for this book.
|
ROLE: Marketing Copywriter
|
||||||
TITLE: {meta.get('title')}
|
TASK: Write a back-cover blurb (150-200 words).
|
||||||
GENRE: {meta.get('genre')}
|
|
||||||
LOGLINE: {bp.get('manual_instruction')}
|
INPUT_DATA:
|
||||||
PLOT: {json.dumps(bp.get('plot_beats', []))}
|
- TITLE: {meta.get('title')}
|
||||||
CHARACTERS: {json.dumps(bp.get('characters', []))}
|
- 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:
|
try:
|
||||||
response = ai.model_writer.generate_content(prompt)
|
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():
|
if feedback and feedback.strip():
|
||||||
utils.log("MARKETING", f"Analyzing feedback: '{feedback}'...")
|
utils.log("MARKETING", f"Analyzing feedback: '{feedback}'...")
|
||||||
analysis_prompt = f"""
|
analysis_prompt = f"""
|
||||||
User Feedback on Book Cover: "{feedback}"
|
ROLE: Design Assistant
|
||||||
Determine if the user wants to:
|
TASK: Analyze user feedback on cover.
|
||||||
|
|
||||||
|
FEEDBACK: "{feedback}"
|
||||||
|
|
||||||
|
DECISION:
|
||||||
1. Keep the current background image but change text/layout/color (REGENERATE_LAYOUT).
|
1. Keep the current background image but change text/layout/color (REGENERATE_LAYOUT).
|
||||||
2. Create a completely new background image (REGENERATE_IMAGE).
|
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.
|
OUTPUT_FORMAT (JSON): {{ "action": "REGENERATE_LAYOUT" or "REGENERATE_IMAGE", "instruction": "Specific instruction for Art Director" }}
|
||||||
Return JSON: {{ "action": "REGENERATE_LAYOUT" or "REGENERATE_IMAGE", "instruction": "Specific instruction for the Art Director" }}
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
resp = ai.model_logic.generate_content(analysis_prompt)
|
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.")
|
utils.log("MARKETING", "Feedback analysis failed. Defaulting to full regeneration.")
|
||||||
|
|
||||||
design_prompt = f"""
|
design_prompt = f"""
|
||||||
Act as an Art Director. Design the cover for this book.
|
ROLE: Art Director
|
||||||
TITLE: {meta.get('title')}
|
TASK: Design a book cover.
|
||||||
GENRE: {meta.get('genre')}
|
|
||||||
TONE: {meta.get('style', {}).get('tone', 'Balanced')}
|
|
||||||
|
|
||||||
CRITICAL INSTRUCTIONS:
|
METADATA:
|
||||||
1. CHARACTER APPEARANCE: Strictly adhere to the provided character descriptions (hair, eyes, race, age, clothing) in the Visual Context.
|
- TITLE: {meta.get('title')}
|
||||||
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).
|
- GENRE: {meta.get('genre')}
|
||||||
|
- TONE: {meta.get('style', {}).get('tone', 'Balanced')}
|
||||||
|
|
||||||
|
VISUAL_CONTEXT:
|
||||||
{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)",
|
"font_name": "Name of a popular Google Font (e.g. Roboto, Cinzel, Oswald, Playfair Display)",
|
||||||
"primary_color": "#HexCode (Background)",
|
"primary_color": "#HexCode (Background)",
|
||||||
@@ -277,10 +294,21 @@ def generate_cover(bp, folder, tracking=None, feedback=None, interactive=False):
|
|||||||
best_layout_path = None
|
best_layout_path = None
|
||||||
|
|
||||||
base_layout_prompt = f"""
|
base_layout_prompt = f"""
|
||||||
Act as a Senior Book Cover Designer. Analyze this 600x900 cover art.
|
ROLE: Graphic Designer
|
||||||
BOOK DETAILS: Title: {meta.get('title')}, Author: {meta.get('author')}, Genre: {meta.get('genre')}
|
TASK: Determine text layout coordinates for a 600x900 cover.
|
||||||
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" }} }}
|
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:
|
if feedback:
|
||||||
@@ -344,7 +372,13 @@ def generate_cover(bp, folder, tracking=None, feedback=None, interactive=False):
|
|||||||
img_copy.save(attempt_path)
|
img_copy.save(attempt_path)
|
||||||
|
|
||||||
# Evaluate Layout
|
# 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)
|
score, critique = evaluate_image_quality(attempt_path, eval_prompt, ai.model_logic, folder)
|
||||||
if score is None: score = 0
|
if score is None: score = 0
|
||||||
|
|
||||||
|
|||||||
556
modules/story.py
556
modules/story.py
@@ -36,20 +36,19 @@ def refresh_style_guidelines(model, folder=None):
|
|||||||
current = get_style_guidelines()
|
current = get_style_guidelines()
|
||||||
|
|
||||||
prompt = f"""
|
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):
|
INPUT_DATA:
|
||||||
{json.dumps(current.get('ai_isms', []))}
|
- CURRENT_AI_ISMS: {json.dumps(current.get('ai_isms', []))}
|
||||||
|
- CURRENT_FILTER_WORDS: {json.dumps(current.get('filter_words', []))}
|
||||||
|
|
||||||
CURRENT FILTER WORDS (Distancing language):
|
INSTRUCTIONS:
|
||||||
{json.dumps(current.get('filter_words', []))}
|
1. Review lists. Remove false positives.
|
||||||
|
|
||||||
TASK:
|
|
||||||
1. Review the lists. Remove any that are too common/safe (false positives).
|
|
||||||
2. Add new common AI tropes (e.g. 'neon-lit', 'bustling', 'a sense of', 'mined', 'delved').
|
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:
|
try:
|
||||||
response = model.generate_content(prompt)
|
response = model.generate_content(prompt)
|
||||||
@@ -131,25 +130,24 @@ def enrich(bp, folder, context=""):
|
|||||||
if 'plot_beats' not in bp: bp['plot_beats'] = []
|
if 'plot_beats' not in bp: bp['plot_beats'] = []
|
||||||
|
|
||||||
prompt = f"""
|
prompt = f"""
|
||||||
You are a Creative Director.
|
ROLE: Creative Director
|
||||||
The user has provided a minimal description. You must build a full Book Bible.
|
TASK: Create a comprehensive Book Bible from the user description.
|
||||||
|
|
||||||
USER DESCRIPTION: "{bp.get('manual_instruction', 'A generic story')}"
|
INPUT DATA:
|
||||||
CONTEXT (Sequel): {context}
|
- USER_DESCRIPTION: "{bp.get('manual_instruction', 'A generic story')}"
|
||||||
|
- CONTEXT (Sequel): {context}
|
||||||
|
|
||||||
TASK:
|
STEPS:
|
||||||
1. Generate a catchy Title.
|
1. Generate a catchy Title.
|
||||||
2. Define the Genre and Tone.
|
2. Define the Genre and Tone.
|
||||||
3. Determine the Time Period (e.g. "Modern", "1920s", "Sci-Fi Future").
|
3. Determine the Time Period (e.g. "Modern", "1920s", "Sci-Fi Future").
|
||||||
4. Define Formatting Rules for text messages, thoughts, and chapter headers.
|
4. Define Formatting Rules for text messages, thoughts, and chapter headers.
|
||||||
5. Create Protagonist and Antagonist/Love Interest.
|
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.
|
- Logic: If sequel, reuse context. If new, create.
|
||||||
- IF NEW CHARACTERS: Create them.
|
|
||||||
- IF RETURNING: Reuse details from CONTEXT.
|
|
||||||
6. Outline 5-7 core Plot Beats.
|
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").
|
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"] }} }},
|
"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"] }} ],
|
"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:
|
if not beats_context:
|
||||||
beats_context = bp.get('plot_beats', [])
|
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:
|
try:
|
||||||
response = ai.model_logic.generate_content(prompt)
|
response = ai.model_logic.generate_content(prompt)
|
||||||
utils.log_usage(folder, "logic-pro", response.usage_metadata)
|
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', [])
|
beats_context = bp.get('plot_beats', [])
|
||||||
|
|
||||||
prompt = f"""
|
prompt = f"""
|
||||||
You are a Story Architect.
|
ROLE: Story Architect
|
||||||
Goal: Flesh out this outline for a {target_chapters}-chapter book.
|
TASK: Expand the outline to fit a {target_chapters}-chapter book.
|
||||||
Current Status: {len(events)} beats.
|
CURRENT_COUNT: {len(events)} beats.
|
||||||
|
|
||||||
ORIGINAL OUTLINE:
|
INPUT_OUTLINE:
|
||||||
{json.dumps(beats_context)}
|
{json.dumps(beats_context)}
|
||||||
|
|
||||||
INSTRUCTIONS:
|
CURRENT_EVENTS:
|
||||||
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:
|
|
||||||
{json.dumps(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:
|
try:
|
||||||
response = ai.model_logic.generate_content(prompt)
|
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)}."
|
pov_instruction = f"- Assign a 'pov_character' for each chapter from this list: {json.dumps(pov_chars)}."
|
||||||
|
|
||||||
prompt = f"""
|
prompt = f"""
|
||||||
Group events into Chapters.
|
ROLE: Pacing Specialist
|
||||||
TARGET CHAPTERS: {target} (Approximate. Feel free to adjust +/- 20% for better pacing).
|
TASK: Group events into Chapters.
|
||||||
TARGET WORDS: {words} (Total for the book).
|
|
||||||
|
|
||||||
INSTRUCTIONS:
|
CONSTRAINTS:
|
||||||
- Vary chapter pacing. Options: 'Very Fast', 'Fast', 'Standard', 'Slow', 'Very Slow'.
|
- TARGET_CHAPTERS: {target}
|
||||||
- Assign an estimated word count to each chapter based on its pacing and content.
|
- TARGET_WORDS: {words}
|
||||||
|
- INSTRUCTIONS:
|
||||||
{structure_instructions}
|
{structure_instructions}
|
||||||
{pov_instruction}
|
{pov_instruction}
|
||||||
|
|
||||||
EVENTS: {json.dumps(events)}
|
INPUT_EVENTS: {json.dumps(events)}
|
||||||
Return JSON: [{{'chapter_number':1, 'title':'...', 'pov_character': 'Name', 'pacing': 'Standard', 'estimated_words': 2000, 'beats':[...]}}]
|
|
||||||
|
OUTPUT_FORMAT (JSON): [{{ "chapter_number": 1, "title": "String", "pov_character": "String", "pacing": "String", "estimated_words": 2000, "beats": ["String"] }}]
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
response = ai.model_logic.generate_content(prompt)
|
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}...")
|
utils.log("TRACKER", f"Updating world state & character visuals for Ch {chapter_num}...")
|
||||||
|
|
||||||
prompt = f"""
|
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)}
|
{json.dumps(current_tracking)}
|
||||||
|
|
||||||
NEW CHAPTER TEXT:
|
NEW_TEXT:
|
||||||
{chapter_text[:500000]}
|
{chapter_text[:500000]}
|
||||||
|
|
||||||
TASK:
|
OPERATIONS:
|
||||||
1. EVENTS: Append 1-3 concise bullet points summarizing key plot events in this chapter to the 'events' list.
|
1. EVENTS: Append 1-3 key plot points to 'events'.
|
||||||
2. CHARACTERS: Update entries for any characters appearing in the scene.
|
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.
|
- "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").
|
- "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".
|
- "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").
|
- "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:
|
try:
|
||||||
response = ai.model_logic.generate_content(prompt)
|
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'])
|
ai_isms = "', '".join(guidelines['ai_isms'])
|
||||||
fw_examples = ", ".join([f"'He {w}'" for w in guidelines['filter_words'][:5]])
|
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"""
|
prompt = f"""
|
||||||
Act as a World-Class Literary Editor (e.g., Maxwell Perkins). Analyze this chapter draft with extreme scrutiny.
|
ROLE: Senior Literary Editor
|
||||||
CHAPTER TITLE: {chapter_title}
|
TASK: Critique chapter draft.
|
||||||
GENRE: {genre}
|
|
||||||
|
|
||||||
STRICT PROHIBITIONS (Automatic deduction):
|
METADATA:
|
||||||
- "AI-isms": '{ai_isms}'. (Evaluate in context of {genre}. Allow genre-appropriate tropes, but penalize robotic clichés).
|
- TITLE: {chapter_title}
|
||||||
- Filter Words: {fw_examples}, etc. (Show the sensation/action, don't state the internal process).
|
- GENRE: {genre}
|
||||||
- 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).
|
|
||||||
|
|
||||||
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?
|
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?
|
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?
|
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?
|
5. SHOW, DON'T TELL: Are emotions shown through physical reactions and subtext?
|
||||||
6. CHARACTER AGENCY: Do characters drive the plot through active choices?
|
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?
|
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: {{
|
OUTPUT_FORMAT (JSON):
|
||||||
'score': int,
|
{{
|
||||||
'critique': 'Detailed analysis of flaws, citing specific examples from the text.',
|
"score": int,
|
||||||
'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").'
|
"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:
|
try:
|
||||||
@@ -431,29 +460,21 @@ def check_pacing(bp, summary, last_chapter_text, last_chapter_data, remaining_ch
|
|||||||
genre = meta.get('genre', 'Fiction')
|
genre = meta.get('genre', 'Fiction')
|
||||||
|
|
||||||
prompt = f"""
|
prompt = f"""
|
||||||
Act as a Senior Structural Editor.
|
ROLE: Structural Editor
|
||||||
We just finished Chapter {last_chapter_data['chapter_number']}: "{last_chapter_data['title']}".
|
TASK: Analyze pacing.
|
||||||
|
|
||||||
STORY SO FAR (Summary):
|
CONTEXT:
|
||||||
{summary[-3000:]}
|
- 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):
|
LOGIC:
|
||||||
{last_chapter_text[-2000:]}
|
- IF skipped major beats -> ADD_BRIDGE
|
||||||
|
- IF covered next chapter's beats -> CUT_NEXT
|
||||||
|
- ELSE -> OK
|
||||||
|
|
||||||
UPCOMING CHAPTERS (Next 3):
|
OUTPUT_FORMAT (JSON):
|
||||||
{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:
|
|
||||||
{{
|
{{
|
||||||
"status": "ok" or "add_bridge" or "cut_next",
|
"status": "ok" or "add_bridge" or "cut_next",
|
||||||
"reason": "Explanation...",
|
"reason": "Explanation...",
|
||||||
@@ -474,17 +495,16 @@ def create_initial_persona(bp, folder):
|
|||||||
style = meta.get('style', {})
|
style = meta.get('style', {})
|
||||||
|
|
||||||
prompt = f"""
|
prompt = f"""
|
||||||
Create a fictional 'Author Persona' best suited to write this book.
|
ROLE: Creative Director
|
||||||
|
TASK: Create a fictional 'Author Persona'.
|
||||||
|
|
||||||
BOOK DETAILS:
|
METADATA:
|
||||||
Title: {meta.get('title')}
|
- TITLE: {meta.get('title')}
|
||||||
Genre: {meta.get('genre')}
|
- GENRE: {meta.get('genre')}
|
||||||
Tone: {style.get('tone')}
|
- TONE: {style.get('tone')}
|
||||||
Target Audience: {meta.get('target_audience')}
|
- AUDIENCE: {meta.get('target_audience')}
|
||||||
|
|
||||||
TASK:
|
OUTPUT_FORMAT (JSON): {{ "name": "Pen Name", "bio": "Description of writing style (voice, sentence structure, vocabulary)...", "age": "...", "gender": "..." }}
|
||||||
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": "..." }}
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
response = ai.model_logic.generate_content(prompt)
|
response = ai.model_logic.generate_content(prompt)
|
||||||
@@ -500,20 +520,16 @@ def refine_persona(bp, text, folder):
|
|||||||
current_bio = ad.get('bio', 'Standard style.')
|
current_bio = ad.get('bio', 'Standard style.')
|
||||||
|
|
||||||
prompt = f"""
|
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:
|
INPUT_DATA:
|
||||||
{text[:3000]}
|
- TEXT_SAMPLE: {text[:3000]}
|
||||||
|
- CURRENT_BIO: {current_bio}
|
||||||
|
|
||||||
CURRENT AUTHOR BIO:
|
GOAL: Ensure future chapters sound exactly like the sample. Highlight quirks, patterns, vocabulary.
|
||||||
{current_bio}
|
|
||||||
|
|
||||||
TASK:
|
OUTPUT_FORMAT (JSON): {{ "bio": "Updated bio..." }}
|
||||||
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..." }}
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
response = ai.model_logic.generate_content(prompt)
|
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():
|
for name, data in tracking['characters'].items():
|
||||||
desc = ", ".join(data.get('descriptors', []))
|
desc = ", ".join(data.get('descriptors', []))
|
||||||
likes = ", ".join(data.get('likes_dislikes', []))
|
likes = ", ".join(data.get('likes_dislikes', []))
|
||||||
|
speech = data.get('speech_style', 'Unknown')
|
||||||
worn = data.get('last_worn', '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', [])
|
major = data.get('major_events', [])
|
||||||
if major: char_visuals += f" * Major Events: {'; '.join(major)}\n"
|
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"
|
prev_context_block = f"\nPREVIOUS CHAPTER TEXT (For Tone & Continuity):\n{trunc_content}\n"
|
||||||
|
|
||||||
prompt = f"""
|
prompt = f"""
|
||||||
Write Chapter {chap['chapter_number']}: {chap['title']}
|
ROLE: Fiction Writer
|
||||||
GENRE: {genre}
|
TASK: Write Chapter {chap['chapter_number']}: {chap['title']}
|
||||||
|
|
||||||
PACING GUIDE:
|
METADATA:
|
||||||
- Format: {ls.get('label', 'Story')}
|
- GENRE: {genre}
|
||||||
- Chapter Pacing: {pacing}
|
- FORMAT: {ls.get('label', 'Story')}
|
||||||
- Target Word Count: ~{est_words} (Use this as a guide, but prioritize story flow. Allow flexibility.)
|
- PACING: {pacing}
|
||||||
- POV Character: {pov_char if pov_char else 'Protagonist'}
|
- TARGET_WORDS: ~{est_words}
|
||||||
|
- POV: {pov_char if pov_char else 'Protagonist'}
|
||||||
|
|
||||||
STYLE & FORMATTING:
|
STYLE_GUIDE:
|
||||||
{style_block}
|
{style_block}
|
||||||
|
|
||||||
AUTHOR VOICE (CRITICAL):
|
AUTHOR_VOICE:
|
||||||
{persona_info}
|
{persona_info}
|
||||||
|
|
||||||
INSTRUCTION:
|
INSTRUCTIONS:
|
||||||
Write the scene.
|
|
||||||
- Start with the Chapter Header formatted as Markdown H1 (e.g. '# Chapter X: Title'). Follow the 'Formatting Rules' for the header style.
|
- Start with the Chapter Header formatted as Markdown H1 (e.g. '# Chapter X: Title'). Follow the 'Formatting Rules' for the header style.
|
||||||
|
|
||||||
|
- 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.
|
- 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.
|
- 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.
|
- 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').
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- 'Very Fast': Rapid fire, pure action/dialogue, minimal description.
|
||||||
- 'Fast': Punchy, keep it moving.
|
- 'Fast': Punchy, keep it moving.
|
||||||
- 'Standard': Balanced dialogue and description.
|
- 'Standard': Balanced dialogue and description.
|
||||||
- 'Slow': Detailed, atmospheric, immersive.
|
- 'Slow': Detailed, atmospheric, immersive.
|
||||||
- 'Very Slow': Deep introspection, heavy sensory detail, slow burn.
|
- '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}
|
{prev_context_block}
|
||||||
CHARACTERS: {json.dumps(bp['characters'])}
|
- CHARACTERS: {json.dumps(bp['characters'])}
|
||||||
{char_visuals}
|
{char_visuals}
|
||||||
SCENE BEATS: {json.dumps(chap['beats'])}
|
- SCENE_BEATS: {json.dumps(chap['beats'])}
|
||||||
|
|
||||||
Output Markdown.
|
OUTPUT: Markdown text.
|
||||||
"""
|
"""
|
||||||
current_text = ""
|
current_text = ""
|
||||||
try:
|
try:
|
||||||
@@ -647,6 +688,7 @@ def write_chapter(chap, bp, folder, prev_sum, tracking=None, prev_content=None):
|
|||||||
max_attempts = 5
|
max_attempts = 5
|
||||||
SCORE_AUTO_ACCEPT = 9
|
SCORE_AUTO_ACCEPT = 9
|
||||||
SCORE_PASSING = 7
|
SCORE_PASSING = 7
|
||||||
|
SCORE_REWRITE_THRESHOLD = 6
|
||||||
|
|
||||||
best_score = 0
|
best_score = 0
|
||||||
best_text = current_text
|
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.")
|
utils.log("WRITER", f" ⚠️ Quality low ({best_score}/{SCORE_PASSING}) but max attempts reached. Proceeding.")
|
||||||
return best_text
|
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...")
|
utils.log("WRITER", f" -> Refining Ch {chap['chapter_number']} based on feedback...")
|
||||||
|
|
||||||
guidelines = get_style_guidelines()
|
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"
|
history_str = "\n".join(past_critiques[:-1]) if len(past_critiques) > 1 else "None"
|
||||||
|
|
||||||
refine_prompt = f"""
|
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}
|
{critique}
|
||||||
|
|
||||||
PREVIOUS CRITIQUES (Reference):
|
HISTORY:
|
||||||
{history_str}
|
{history_str}
|
||||||
|
|
||||||
STYLIZED REWRITE INSTRUCTIONS:
|
CONSTRAINTS:
|
||||||
1. REMOVE FILTER WORDS: Delete "{fw_list}". Describe the image, sensation, or sound directly.
|
{persona_info}
|
||||||
2. VARY SENTENCE STRUCTURE: Do not start consecutive sentences with "He", "She", or "The". Use introductory clauses and varying lengths.
|
{style_block}
|
||||||
3. SUBTEXT: Ensure dialogue implies meaning rather than stating it outright. People rarely say exactly what they mean.
|
{char_visuals}
|
||||||
4. GENRE CONSISTENCY: Ensure the tone matches {meta.get('genre', 'Fiction')}.
|
- BEATS: {json.dumps(chap.get('beats', []))}
|
||||||
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.
|
|
||||||
|
|
||||||
STORY SO FAR:
|
OPTIMIZATION_RULES:
|
||||||
{prev_sum}
|
1. NO_FILTERS: Remove [{fw_list}].
|
||||||
{prev_context_block}
|
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:
|
INPUT_CONTEXT:
|
||||||
{current_text}
|
- 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:
|
try:
|
||||||
# Use Logic model (Pro) for refinement to ensure higher quality prose
|
# 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]
|
full_text = "\n".join([c.get('content', '') for c in full_manuscript])[:500000]
|
||||||
|
|
||||||
prompt = f"""
|
prompt = f"""
|
||||||
Analyze this manuscript text.
|
ROLE: Data Extractor
|
||||||
EXISTING CHARACTERS: {json.dumps(bp['characters'])}
|
TASK: Identify NEW significant characters.
|
||||||
|
|
||||||
TASK: Identify NEW significant characters that appear in the text but are missing from the list.
|
INPUT_TEXT:
|
||||||
RETURN JSON: {{'new_characters': [{{'name':'...', 'role':'...', 'description':'...'}}]}}
|
{full_text}
|
||||||
|
|
||||||
|
KNOWN_CHARACTERS: {json.dumps(bp['characters'])}
|
||||||
|
|
||||||
|
OUTPUT_FORMAT (JSON): {{ "new_characters": [{{ "name": "String", "role": "String", "description": "String" }}] }}
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
response = ai.model_logic.generate_content(prompt)
|
response = ai.model_logic.generate_content(prompt)
|
||||||
@@ -786,7 +859,12 @@ def update_persona_sample(bp, folder):
|
|||||||
|
|
||||||
if author_name not in personas:
|
if author_name not in personas:
|
||||||
utils.log("SYSTEM", f"Generating new persona profile for '{author_name}'...")
|
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:
|
try:
|
||||||
response = ai.model_logic.generate_content(prompt)
|
response = ai.model_logic.generate_content(prompt)
|
||||||
utils.log_usage(folder, "logic-pro", response.usage_metadata)
|
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):
|
def refine_bible(bible, instruction, folder):
|
||||||
utils.log("SYSTEM", f"Refining Bible with instruction: {instruction}")
|
utils.log("SYSTEM", f"Refining Bible with instruction: {instruction}")
|
||||||
prompt = f"""
|
prompt = f"""
|
||||||
Act as a Senior Developmental Editor.
|
ROLE: Senior Developmental Editor
|
||||||
CURRENT JSON: {json.dumps(bible)}
|
TASK: Update the Bible JSON based on instruction.
|
||||||
USER INSTRUCTION: {instruction}
|
|
||||||
|
|
||||||
TASK: Update the JSON based on the instruction. Maintain valid JSON structure.
|
INPUT_DATA:
|
||||||
Ensure character motivations remain consistent and plot holes are avoided.
|
- 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:
|
try:
|
||||||
response = ai.model_logic.generate_content(prompt)
|
response = ai.model_logic.generate_content(prompt)
|
||||||
@@ -845,18 +927,15 @@ def analyze_consistency(bp, manuscript, folder):
|
|||||||
context = "\n".join(chapter_summaries)
|
context = "\n".join(chapter_summaries)
|
||||||
|
|
||||||
prompt = f"""
|
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', []))}
|
INPUT_DATA:
|
||||||
|
- CHARACTERS: {json.dumps(bp.get('characters', []))}
|
||||||
CHAPTER SUMMARIES:
|
- SUMMARIES:
|
||||||
{context}
|
{context}
|
||||||
|
|
||||||
TASK:
|
OUTPUT_FORMAT (JSON): {{ "issues": ["Issue 1", "Issue 2"], "score": 8, "summary": "Brief overall assessment." }}
|
||||||
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)
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
response = ai.model_logic.generate_content(prompt)
|
response = ai.model_logic.generate_content(prompt)
|
||||||
@@ -888,51 +967,110 @@ def rewrite_chapter_content(bp, manuscript, chapter_num, instruction, folder):
|
|||||||
prev_text = prev_chap.get('content', '')[-3000:] # Last 3000 chars for context
|
prev_text = prev_chap.get('content', '')[-3000:] # Last 3000 chars for context
|
||||||
|
|
||||||
meta = bp.get('book_metadata', {})
|
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"""
|
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}
|
{instruction}
|
||||||
|
|
||||||
STORY CONTEXT:
|
EXECUTION RULES:
|
||||||
- Title: {meta.get('title')}
|
1. CONTINUITY: The new text must flow logically from PREVIOUS_CONTEXT.
|
||||||
- Genre: {meta.get('genre')}
|
2. ADHERENCE: The PRIMARY DIRECTIVE overrides any conflicting details in CURRENT_DRAFT.
|
||||||
- Tone: {meta.get('style', {}).get('tone')}
|
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):
|
PROSE OPTIMIZATION RULES (STRICT ENFORCEMENT):
|
||||||
{prev_text}
|
- 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):
|
RETURN JSON:
|
||||||
{target_chap.get('content', '')[:5000]}
|
{{
|
||||||
|
"content": "The full chapter text in Markdown...",
|
||||||
CHARACTERS:
|
"summary": "A concise summary of the chapter's events and ending state (for continuity checks)."
|
||||||
{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.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = ai.model_writer.generate_content(prompt)
|
response = ai.model_writer.generate_content(prompt)
|
||||||
utils.log_usage(folder, "writer-flash", response.usage_metadata)
|
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:
|
except Exception as e:
|
||||||
utils.log("WRITER", f"Rewrite failed: {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}...")
|
utils.log("WRITER", f"Checking ripple effects from Ch {changed_chap_num}...")
|
||||||
|
|
||||||
# Find the changed chapter
|
# Find the changed chapter
|
||||||
changed_chap = next((c for c in manuscript if c['num'] == changed_chap_num), None)
|
changed_chap = next((c for c in manuscript if c['num'] == changed_chap_num), None)
|
||||||
if not changed_chap: return None
|
if not changed_chap: return None
|
||||||
|
|
||||||
# Summarize the change to save tokens
|
if change_summary:
|
||||||
change_summary_prompt = f"Summarize the key events and ending state of this chapter:\n{changed_chap.get('content', '')[:10000]}"
|
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:
|
try:
|
||||||
resp = ai.model_writer.generate_content(change_summary_prompt)
|
resp = ai.model_writer.generate_content(change_summary_prompt)
|
||||||
current_context = resp.text
|
current_context = resp.text
|
||||||
@@ -979,20 +1117,18 @@ def check_and_propagate(bp, manuscript, changed_chap_num, folder):
|
|||||||
chapter_summaries.append(f"Ch {rc['num']}: {excerpt}")
|
chapter_summaries.append(f"Ch {rc['num']}: {excerpt}")
|
||||||
|
|
||||||
scan_prompt = f"""
|
scan_prompt = f"""
|
||||||
We are propagating a change from Chapter {changed_chap_num}.
|
ROLE: Continuity Scanner
|
||||||
The immediate ripple effect seems to have stopped.
|
TASK: Identify chapters impacted by a change.
|
||||||
|
|
||||||
ORIGINAL CHANGE CONTEXT:
|
CHANGE_CONTEXT:
|
||||||
{original_change_context}
|
{original_change_context}
|
||||||
|
|
||||||
REMAINING CHAPTERS:
|
CHAPTER_SUMMARIES:
|
||||||
{json.dumps(chapter_summaries)}
|
{json.dumps(chapter_summaries)}
|
||||||
|
|
||||||
TASK:
|
CRITERIA: Identify later chapters that mention items, characters, or locations involved in the Change Context.
|
||||||
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.
|
OUTPUT_FORMAT (JSON): [Chapter_Number_Int, ...]
|
||||||
Example: [5, 12]
|
|
||||||
If none, return [].
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -1020,34 +1156,44 @@ def check_and_propagate(bp, manuscript, changed_chap_num, folder):
|
|||||||
utils.log("WRITER", f" -> Checking Ch {target_chap['num']} for continuity...")
|
utils.log("WRITER", f" -> Checking Ch {target_chap['num']} for continuity...")
|
||||||
|
|
||||||
prompt = f"""
|
prompt = f"""
|
||||||
Chapter {changed_chap_num} was just rewritten.
|
ROLE: Continuity Checker
|
||||||
NEW CONTEXT/ENDING of previous section:
|
TASK: Determine if chapter needs rewrite based on new context.
|
||||||
{current_context}
|
|
||||||
|
|
||||||
CURRENT TEXT of Ch {target_chap['num']}:
|
INPUT_DATA:
|
||||||
{target_chap['content'][:5000]}... (truncated)
|
- CHANGED_CHAPTER: {changed_chap_num}
|
||||||
|
- NEW_CONTEXT: {current_context}
|
||||||
|
- CURRENT_CHAPTER_TEXT: {target_chap['content'][:5000]}...
|
||||||
|
|
||||||
TASK:
|
DECISION_LOGIC:
|
||||||
Does Ch {target_chap['num']} need to be rewritten to maintain continuity with the new context?
|
- Compare CURRENT_CHAPTER_TEXT with NEW_CONTEXT.
|
||||||
- If YES (e.g. references old events that changed, character states don't match): Rewrite the chapter fully in Markdown.
|
- 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 NO (it fits fine): Return ONLY the string "NO_CHANGE".
|
- 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:
|
try:
|
||||||
response = ai.model_writer.generate_content(prompt)
|
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.")
|
utils.log("WRITER", f" -> Ch {target_chap['num']} is consistent.")
|
||||||
# Update context for next iteration using existing text
|
# Update context for next iteration using existing text
|
||||||
current_context = f"Ch {target_chap['num']} Summary: " + target_chap.get('content', '')[-2000:]
|
current_context = f"Ch {target_chap['num']} Summary: " + target_chap.get('content', '')[-2000:]
|
||||||
consecutive_no_changes += 1
|
consecutive_no_changes += 1
|
||||||
else:
|
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.")
|
utils.log("WRITER", f" -> Rewriting Ch {target_chap['num']} to fix continuity.")
|
||||||
target_chap['content'] = text
|
target_chap['content'] = new_text
|
||||||
changes_made = True
|
changes_made = True
|
||||||
# Update context with NEW text
|
# Update context with NEW text
|
||||||
current_context = f"Ch {target_chap['num']} Summary: " + text[-2000:]
|
current_context = f"Ch {target_chap['num']} Summary: " + new_text[-2000:]
|
||||||
consecutive_no_changes = 0
|
consecutive_no_changes = 0
|
||||||
|
|
||||||
# Save immediately to prevent data loss if subsequent checks fail
|
# Save immediately to prevent data loss if subsequent checks fail
|
||||||
|
|||||||
@@ -162,32 +162,36 @@ def project_setup_wizard():
|
|||||||
return redirect(url_for('index'))
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
prompt = f"""
|
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}
|
CONCEPT: {concept}
|
||||||
|
|
||||||
RETURN JSON with these keys:
|
OUTPUT_FORMAT (JSON):
|
||||||
- title: Suggested book title
|
{{
|
||||||
- genre: Genre
|
"title": "String",
|
||||||
- target_audience: e.g. Adult, YA
|
"genre": "String",
|
||||||
- tone: e.g. Dark, Whimsical
|
"target_audience": "String",
|
||||||
- length_category: One of ["01", "1", "2", "2b", "3", "4", "5"] based on likely depth.
|
"tone": "String",
|
||||||
- estimated_chapters: int (suggested chapter count)
|
"length_category": "String (Select code: '01'=Chapter Book, '1'=Flash Fiction, '2'=Short Story, '2b'=Young Adult, '3'=Novella, '4'=Novel, '5'=Epic)",
|
||||||
- estimated_word_count: string (e.g. "75,000")
|
"estimated_chapters": Int,
|
||||||
- include_prologue: boolean
|
"estimated_word_count": "String (e.g. '75,000')",
|
||||||
- include_epilogue: boolean
|
"include_prologue": Bool,
|
||||||
- tropes: list of strings
|
"include_epilogue": Bool,
|
||||||
- pov_style: e.g. First Person
|
"tropes": ["String"],
|
||||||
- time_period: e.g. Modern
|
"pov_style": "String",
|
||||||
- spice: e.g. Standard, Explicit
|
"time_period": "String",
|
||||||
- violence: e.g. None, Graphic
|
"spice": "String",
|
||||||
- is_series: boolean
|
"violence": "String",
|
||||||
- series_title: string (if series)
|
"is_series": Bool,
|
||||||
- narrative_tense: e.g. Past, Present
|
"series_title": "String",
|
||||||
- language_style: e.g. Standard, Flowery
|
"narrative_tense": "String",
|
||||||
- dialogue_style: e.g. Witty, Formal
|
"language_style": "String",
|
||||||
- page_orientation: Portrait, Landscape, or Square
|
"dialogue_style": "String",
|
||||||
- formatting_rules: list of strings
|
"page_orientation": "Portrait|Landscape|Square",
|
||||||
- author_bio: string (suggested persona bio)
|
"formatting_rules": ["String (e.g. 'Chapter Headers: Number + Title')"],
|
||||||
|
"author_bio": "String"
|
||||||
|
}}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
suggestions = {}
|
suggestions = {}
|
||||||
@@ -227,12 +231,15 @@ def project_setup_refine():
|
|||||||
except: pass
|
except: pass
|
||||||
|
|
||||||
prompt = f"""
|
prompt = f"""
|
||||||
Update these project suggestions based on the user instruction.
|
ROLE: Publishing Analyst
|
||||||
ORIGINAL CONCEPT: {concept}
|
TASK: Refine project metadata based on user instruction.
|
||||||
CURRENT TITLE: {current_state['title']}
|
|
||||||
INSTRUCTION: {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 = {}
|
suggestions = {}
|
||||||
@@ -1393,19 +1400,20 @@ def analyze_persona():
|
|||||||
sample = data.get('sample_text', '')
|
sample = data.get('sample_text', '')
|
||||||
|
|
||||||
prompt = f"""
|
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:
|
INPUT_DATA:
|
||||||
Name: {data.get('name')}
|
- NAME: {data.get('name')}
|
||||||
Age: {data.get('age')} | Gender: {data.get('gender')} | Nationality: {data.get('nationality')}
|
- DEMOGRAPHICS: Age: {data.get('age')} | Gender: {data.get('gender')} | Nationality: {data.get('nationality')}
|
||||||
Sample Text: {sample[:3000]}
|
- SAMPLE_TEXT: {sample[:3000]}
|
||||||
|
|
||||||
TASK:
|
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.
|
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).
|
2. 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.
|
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:
|
try:
|
||||||
response = ai.model_logic.generate_content(prompt)
|
response = ai.model_logic.generate_content(prompt)
|
||||||
|
|||||||
@@ -350,9 +350,10 @@ def rewrite_chapter_task(run_id, project_path, book_folder, chap_num, instructio
|
|||||||
|
|
||||||
ai.init_models()
|
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:
|
for ch in ms:
|
||||||
if ch.get('num') == chap_num:
|
if ch.get('num') == chap_num:
|
||||||
ch['content'] = new_text
|
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
|
# Save the primary rewrite immediately
|
||||||
with open(ms_path, 'w') as f: json.dump(ms, f, indent=2)
|
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:
|
if updated_ms:
|
||||||
ms = updated_ms
|
ms = updated_ms
|
||||||
|
|
||||||
|
|||||||
99
wizard.py
99
wizard.py
@@ -183,31 +183,35 @@ class BookWizard:
|
|||||||
if concept:
|
if concept:
|
||||||
with console.status("[bold yellow]AI is analyzing your concept...[/bold yellow]"):
|
with console.status("[bold yellow]AI is analyzing your concept...[/bold yellow]"):
|
||||||
prompt = f"""
|
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}
|
CONCEPT: {concept}
|
||||||
|
|
||||||
RETURN JSON with these keys:
|
OUTPUT_FORMAT (JSON):
|
||||||
- title: Suggested book title
|
{{
|
||||||
- genre: Genre
|
"title": "String",
|
||||||
- target_audience: e.g. Adult, YA
|
"genre": "String",
|
||||||
- tone: e.g. Dark, Whimsical
|
"target_audience": "String",
|
||||||
- length_category: One of ["00", "0", "01", "1", "2", "2b", "3", "4", "5"] based on likely depth.
|
"tone": "String",
|
||||||
- estimated_chapters: int (suggested chapter count)
|
"length_category": "String (Select code: '01'=Chapter Book, '1'=Flash Fiction, '2'=Short Story, '2b'=Young Adult, '3'=Novella, '4'=Novel, '5'=Epic)",
|
||||||
- estimated_word_count: string (e.g. "75,000")
|
"estimated_chapters": Int,
|
||||||
- include_prologue: boolean
|
"estimated_word_count": "String (e.g. '75,000')",
|
||||||
- include_epilogue: boolean
|
"include_prologue": Bool,
|
||||||
- tropes: list of strings
|
"include_epilogue": Bool,
|
||||||
- pov_style: e.g. First Person
|
"tropes": ["String"],
|
||||||
- time_period: e.g. Modern
|
"pov_style": "String",
|
||||||
- spice: e.g. Standard, Explicit
|
"time_period": "String",
|
||||||
- violence: e.g. None, Graphic
|
"spice": "String",
|
||||||
- is_series: boolean
|
"violence": "String",
|
||||||
- series_title: string (if series)
|
"is_series": Bool,
|
||||||
- narrative_tense: e.g. Past, Present
|
"series_title": "String",
|
||||||
- language_style: e.g. Standard, Flowery
|
"narrative_tense": "String",
|
||||||
- dialogue_style: e.g. Witty, Formal
|
"language_style": "String",
|
||||||
- page_orientation: Portrait, Landscape, or Square
|
"dialogue_style": "String",
|
||||||
- formatting_rules: list of strings
|
"page_orientation": "Portrait|Landscape|Square",
|
||||||
|
"formatting_rules": ["String (e.g. 'Chapter Headers: Number + Title')"]
|
||||||
|
}}
|
||||||
"""
|
"""
|
||||||
suggestions = self.ask_gemini_json(prompt)
|
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')")
|
instruction = Prompt.ask("Instruction (e.g. 'Make it darker', 'Change genre to Sci-Fi')")
|
||||||
with console.status("[bold yellow]Refining suggestions...[/bold yellow]"):
|
with console.status("[bold yellow]Refining suggestions...[/bold yellow]"):
|
||||||
refine_prompt = f"""
|
refine_prompt = f"""
|
||||||
Update these project suggestions based on the user instruction.
|
ROLE: Publishing Analyst
|
||||||
CURRENT JSON: {json.dumps(suggestions)}
|
TASK: Refine project metadata based on user instruction.
|
||||||
INSTRUCTION: {instruction}
|
|
||||||
RETURN ONLY VALID JSON with the same keys.
|
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)
|
new_sugg = self.ask_gemini_json(refine_prompt)
|
||||||
if new_sugg: suggestions = new_sugg
|
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]")
|
console.print("\n[bold yellow]✨ Generating full Book Bible (Characters, Plot, etc.)...[/bold yellow]")
|
||||||
|
|
||||||
prompt = f"""
|
prompt = f"""
|
||||||
You are a Creative Director.
|
ROLE: Creative Director
|
||||||
Create a comprehensive Book Bible for the following project.
|
TASK: Create a comprehensive Book Bible.
|
||||||
|
|
||||||
PROJECT METADATA: {json.dumps(self.data['project_metadata'])}
|
INPUT_DATA:
|
||||||
EXISTING BOOKS STRUCTURE: {json.dumps(self.data['books'])}
|
- METADATA: {json.dumps(self.data['project_metadata'])}
|
||||||
|
- BOOKS: {json.dumps(self.data['books'])}
|
||||||
|
|
||||||
TASK:
|
INSTRUCTIONS:
|
||||||
1. Create a list of Main Characters (Global for the project).
|
1. Create Main Characters.
|
||||||
2. For EACH book in the 'books' list:
|
2. For EACH book: Generate Title, Plot Summary (manual_instruction), and 10-15 Plot Beats.
|
||||||
- Generate a catchy Title (if not provided).
|
|
||||||
- Write a 'manual_instruction' (Plot Summary).
|
|
||||||
- Generate 'plot_beats' (10-15 chronological beats).
|
|
||||||
|
|
||||||
RETURN JSON in standard Bible format:
|
OUTPUT_FORMAT (JSON):
|
||||||
{{
|
{{
|
||||||
"characters": [ {{ "name": "...", "role": "...", "description": "..." }} ],
|
"characters": [ {{ "name": "String", "role": "String", "description": "String" }} ],
|
||||||
"books": [
|
"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:
|
while True:
|
||||||
with console.status("[bold green]AI is updating blueprint...[/bold green]"):
|
with console.status("[bold green]AI is updating blueprint...[/bold green]"):
|
||||||
prompt = f"""
|
prompt = f"""
|
||||||
Act as a Book Editor.
|
ROLE: Senior Editor
|
||||||
CURRENT JSON: {json.dumps(current_data)}
|
TASK: Update the Bible JSON based on instruction.
|
||||||
USER INSTRUCTION: {instruction}
|
|
||||||
|
|
||||||
TASK: Update the JSON based on the instruction. Maintain valid JSON structure.
|
INPUT_DATA:
|
||||||
RETURN ONLY THE JSON.
|
- CURRENT_JSON: {json.dumps(current_data)}
|
||||||
|
- INSTRUCTION: {instruction}
|
||||||
|
|
||||||
|
OUTPUT_FORMAT (JSON): The full updated JSON object.
|
||||||
"""
|
"""
|
||||||
new_data = self.ask_gemini_json(prompt)
|
new_data = self.ask_gemini_json(prompt)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user