Fixes for site
This commit is contained in:
152
modules/story.py
152
modules/story.py
@@ -6,6 +6,71 @@ import config
|
||||
from modules import ai
|
||||
from . import utils
|
||||
|
||||
def get_style_guidelines():
|
||||
defaults = {
|
||||
"ai_isms": [
|
||||
'testament to', 'tapestry', 'shiver down spine', 'unspoken agreement',
|
||||
'palpable tension', 'a sense of', 'suddenly', 'in that moment',
|
||||
'symphony of', 'dance of', 'azure', 'cerulean'
|
||||
],
|
||||
"filter_words": [
|
||||
'felt', 'saw', 'heard', 'realized', 'decided', 'noticed', 'knew', 'thought'
|
||||
]
|
||||
}
|
||||
path = os.path.join(config.DATA_DIR, "style_guidelines.json")
|
||||
if os.path.exists(path):
|
||||
try:
|
||||
user_data = utils.load_json(path)
|
||||
if user_data:
|
||||
if 'ai_isms' in user_data: defaults['ai_isms'] = user_data['ai_isms']
|
||||
if 'filter_words' in user_data: defaults['filter_words'] = user_data['filter_words']
|
||||
except: pass
|
||||
else:
|
||||
try:
|
||||
with open(path, 'w') as f: json.dump(defaults, f, indent=2)
|
||||
except: pass
|
||||
return defaults
|
||||
|
||||
def refresh_style_guidelines(model, folder=None):
|
||||
utils.log("SYSTEM", "Refreshing Style Guidelines via AI...")
|
||||
current = get_style_guidelines()
|
||||
|
||||
prompt = f"""
|
||||
Act as a Literary Editor. Update our 'Banned Words' lists for AI writing.
|
||||
|
||||
CURRENT AI-ISMS (Cliches to avoid):
|
||||
{json.dumps(current.get('ai_isms', []))}
|
||||
|
||||
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).
|
||||
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.
|
||||
|
||||
RETURN JSON: {{ "ai_isms": [strings], "filter_words": [strings] }}
|
||||
"""
|
||||
try:
|
||||
response = model.generate_content(prompt)
|
||||
if folder: utils.log_usage(folder, "logic-pro", response.usage_metadata)
|
||||
new_data = json.loads(utils.clean_json(response.text))
|
||||
|
||||
# Validate
|
||||
if 'ai_isms' in new_data and 'filter_words' in new_data:
|
||||
path = os.path.join(config.DATA_DIR, "style_guidelines.json")
|
||||
with open(path, 'w') as f: json.dump(new_data, f, indent=2)
|
||||
utils.log("SYSTEM", "Style Guidelines updated.")
|
||||
return new_data
|
||||
except Exception as e:
|
||||
utils.log("SYSTEM", f"Failed to refresh guidelines: {e}")
|
||||
return current
|
||||
|
||||
def filter_characters(chars):
|
||||
"""Removes placeholder characters generated by AI."""
|
||||
blacklist = ['name', 'character name', 'role', 'protagonist', 'antagonist', 'love interest', 'unknown', 'tbd', 'todo', 'hero', 'villain', 'main character', 'side character']
|
||||
return [c for c in chars if c.get('name') and c.get('name').lower().strip() not in blacklist]
|
||||
|
||||
def enrich(bp, folder, context=""):
|
||||
utils.log("ENRICHER", "Fleshing out details from description...")
|
||||
|
||||
@@ -36,7 +101,7 @@ def enrich(bp, folder, context=""):
|
||||
RETURN JSON in this EXACT format:
|
||||
{{
|
||||
"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": "Name", "role": "Role", "description": "Description", "key_events": ["Planned injury in Act 2"] }} ],
|
||||
"characters": [ {{ "name": "John Doe", "role": "Protagonist", "description": "Description", "key_events": ["Planned injury in Act 2"] }} ],
|
||||
"plot_beats": [ "Beat 1", "Beat 2", "..." ]
|
||||
}}
|
||||
"""
|
||||
@@ -72,6 +137,11 @@ def enrich(bp, folder, context=""):
|
||||
|
||||
if 'characters' not in bp or not bp['characters']:
|
||||
bp['characters'] = ai_data.get('characters', [])
|
||||
|
||||
# Filter out default names
|
||||
if 'characters' in bp:
|
||||
bp['characters'] = filter_characters(bp['characters'])
|
||||
|
||||
if 'plot_beats' not in bp or not bp['plot_beats']:
|
||||
bp['plot_beats'] = ai_data.get('plot_beats', [])
|
||||
|
||||
@@ -281,26 +351,45 @@ def update_tracking(folder, chapter_num, chapter_text, current_tracking):
|
||||
return current_tracking
|
||||
|
||||
def evaluate_chapter_quality(text, chapter_title, model, folder):
|
||||
guidelines = get_style_guidelines()
|
||||
ai_isms = "', '".join(guidelines['ai_isms'])
|
||||
fw_examples = ", ".join([f"'He {w}'" for w in guidelines['filter_words'][:5]])
|
||||
|
||||
prompt = f"""
|
||||
Analyze this book chapter text.
|
||||
Act as a World-Class Literary Editor (e.g., Maxwell Perkins). Analyze this chapter draft with extreme scrutiny.
|
||||
CHAPTER TITLE: {chapter_title}
|
||||
|
||||
STRICT PROHIBITIONS (Automatic deduction):
|
||||
- "AI-isms": '{ai_isms}'.
|
||||
- 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.
|
||||
|
||||
CRITERIA:
|
||||
1. ORGANIC FEEL: Does it sound like a human wrote it? Are "AI-isms" (e.g. 'testament to', 'tapestry', 'shiver down spine', 'unspoken agreement') absent?
|
||||
2. ENGAGEMENT: Is it interesting? Does it hook the reader?
|
||||
3. REPETITION: Is sentence structure varied? Are words repeated unnecessarily?
|
||||
4. PROGRESSION: Does the story move forward, or is it spinning its wheels?
|
||||
1. VOICE & TONE: Is the narrative voice distinct, or generic? Does it match the genre?
|
||||
2. SHOW, DON'T TELL: Are emotions demonstrated through action/viscera, or summarized?
|
||||
3. PACING: Does the scene drag? Is there conflict in every beat?
|
||||
4. CHARACTER AGENCY: Do characters make choices, or do things just happen to them?
|
||||
|
||||
Rate on a scale of 1-10.
|
||||
Provide a concise critique focusing on the biggest flaw.
|
||||
Rate on a scale of 1-10. (Be harsh. 10 is Pulitzer level. 6 is average. Anything below 8 needs work).
|
||||
|
||||
Return JSON: {{'score': int, 'critique': 'string'}}
|
||||
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. "Cut the first 3 paragraphs", "Make the dialogue in the middle argument more aggressive", "Describe the smell of the room").'
|
||||
}}
|
||||
"""
|
||||
try:
|
||||
response = model.generate_content([prompt, text[:30000]])
|
||||
utils.log_usage(folder, "logic-pro", response.usage_metadata)
|
||||
data = json.loads(utils.clean_json(response.text))
|
||||
return data.get('score', 0), data.get('critique', 'No critique provided.')
|
||||
|
||||
critique_text = data.get('critique', 'No critique provided.')
|
||||
if data.get('actionable_feedback'):
|
||||
critique_text += "\n\nREQUIRED FIXES:\n" + str(data.get('actionable_feedback'))
|
||||
|
||||
return data.get('score', 0), critique_text
|
||||
except Exception as e:
|
||||
return 0, f"Evaluation error: {str(e)}"
|
||||
|
||||
@@ -336,7 +425,7 @@ def refine_persona(bp, text, folder):
|
||||
current_bio = ad.get('bio', 'Standard style.')
|
||||
|
||||
prompt = f"""
|
||||
Analyze this text sample from the book.
|
||||
Act as a Literary Stylist. Analyze this text sample from the book.
|
||||
|
||||
TEXT:
|
||||
{text[:3000]}
|
||||
@@ -449,7 +538,7 @@ def write_chapter(chap, bp, folder, prev_sum, tracking=None, prev_content=None):
|
||||
- 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.
|
||||
- SENSORY DETAILS: Use specific, grounding sensory details (smell, touch, sound) rather than generic descriptions.
|
||||
- AVOID CLICHÉS: Avoid common AI tropes (e.g., 'shiver down spine', 'palpable tension', 'unspoken agreement', 'testament to').
|
||||
- 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.
|
||||
@@ -477,14 +566,17 @@ def write_chapter(chap, bp, folder, prev_sum, tracking=None, prev_content=None):
|
||||
return f"## Chapter {chap['chapter_number']} Failed\n\nError: {e}"
|
||||
|
||||
# Refinement Loop
|
||||
max_attempts = 3
|
||||
max_attempts = 9
|
||||
best_score = 0
|
||||
best_text = current_text
|
||||
past_critiques = []
|
||||
|
||||
for attempt in range(1, max_attempts + 1):
|
||||
utils.log("WRITER", f" -> Evaluating Ch {chap['chapter_number']} (Attempt {attempt}/{max_attempts})...")
|
||||
score, critique = evaluate_chapter_quality(current_text, chap['title'], ai.model_logic, folder)
|
||||
|
||||
past_critiques.append(f"Attempt {attempt}: {critique}")
|
||||
|
||||
if "Evaluation error" in critique:
|
||||
utils.log("WRITER", f" ⚠️ {critique}. Keeping current draft.")
|
||||
if best_score == 0: best_text = current_text
|
||||
@@ -505,17 +597,27 @@ def write_chapter(chap, bp, folder, prev_sum, tracking=None, prev_content=None):
|
||||
return best_text
|
||||
|
||||
utils.log("WRITER", f" -> Refining Ch {chap['chapter_number']} based on feedback...")
|
||||
|
||||
guidelines = get_style_guidelines()
|
||||
fw_list = '", "'.join(guidelines['filter_words'])
|
||||
|
||||
history_str = "\n".join(past_critiques)
|
||||
|
||||
refine_prompt = f"""
|
||||
Act as a Senior Editor. Rewrite this chapter to fix the issues identified below.
|
||||
|
||||
CRITIQUE TO ADDRESS:
|
||||
CRITIQUE TO ADDRESS (MANDATORY):
|
||||
{critique}
|
||||
|
||||
ADDITIONAL OBJECTIVES:
|
||||
1. NATURAL FLOW: Fix stilted phrasing. Ensure the prose flows naturally for the genre ({meta.get('genre', 'Fiction')}) and tone ({style.get('tone', 'Standard')}).
|
||||
2. HUMANIZATION: Remove robotic phrasing. Ensure dialogue has subtext, interruptions, and distinct voices. Remove "AI-isms" (e.g. 'testament to', 'tapestry of', 'symphony of').
|
||||
3. SENTENCE VARIETY: Check for and fix repetitive sentence starts or uniform sentence lengths. The prose should have a dynamic rhythm.
|
||||
4. CONTINUITY: Ensure consistency with the Story So Far.
|
||||
PREVIOUS CRITIQUES (Reference):
|
||||
{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.
|
||||
|
||||
STORY SO FAR:
|
||||
{prev_sum}
|
||||
@@ -544,9 +646,11 @@ def harvest_metadata(bp, folder, full_manuscript):
|
||||
response = ai.model_logic.generate_content(prompt)
|
||||
utils.log_usage(folder, "logic-pro", response.usage_metadata)
|
||||
new_chars = json.loads(utils.clean_json(response.text)).get('new_characters', [])
|
||||
if new_chars:
|
||||
utils.log("HARVESTER", f"Found {len(new_chars)} new chars.")
|
||||
bp['characters'].extend(new_chars)
|
||||
if new_chars:
|
||||
valid_chars = filter_characters(new_chars)
|
||||
if valid_chars:
|
||||
utils.log("HARVESTER", f"Found {len(valid_chars)} new chars.")
|
||||
bp['characters'].extend(valid_chars)
|
||||
except: pass
|
||||
return bp
|
||||
|
||||
@@ -609,11 +713,13 @@ 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 Book Editor.
|
||||
Act as a Senior Developmental Editor.
|
||||
CURRENT JSON: {json.dumps(bible)}
|
||||
USER INSTRUCTION: {instruction}
|
||||
|
||||
TASK: Update the JSON based on the instruction. Maintain valid JSON structure.
|
||||
Ensure character motivations remain consistent and plot holes are avoided.
|
||||
|
||||
RETURN ONLY THE JSON.
|
||||
"""
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user