Final changes and update
This commit is contained in:
295
modules/story.py
295
modules/story.py
@@ -153,20 +153,6 @@ def enrich(bp, folder, context=""):
|
||||
def plan_structure(bp, folder):
|
||||
utils.log("ARCHITECT", "Creating structure...")
|
||||
|
||||
if 'plot_outline' in bp and isinstance(bp['plot_outline'], dict):
|
||||
po = bp['plot_outline']
|
||||
if 'beats' in po and isinstance(po['beats'], list):
|
||||
events = []
|
||||
for act in po['beats']:
|
||||
if 'plot_points' in act and isinstance(act['plot_points'], list):
|
||||
for pp in act['plot_points']:
|
||||
desc = pp.get('description')
|
||||
point = pp.get('point', 'Event')
|
||||
if desc: events.append({"description": desc, "purpose": point})
|
||||
if events:
|
||||
utils.log("ARCHITECT", f"Using {len(events)} events from Plot Outline as base structure.")
|
||||
return events
|
||||
|
||||
structure_type = bp.get('book_metadata', {}).get('structure_prompt')
|
||||
|
||||
if not structure_type:
|
||||
@@ -183,13 +169,6 @@ def plan_structure(bp, folder):
|
||||
structure_type = structures.get(label, "Create a 3-Act Structure.")
|
||||
|
||||
beats_context = []
|
||||
if 'plot_outline' in bp and isinstance(bp['plot_outline'], dict):
|
||||
po = bp['plot_outline']
|
||||
if 'beats' in po:
|
||||
for act in po['beats']:
|
||||
beats_context.append(f"ACT {act.get('act', '?')}: {act.get('title', '')} - {act.get('summary', '')}")
|
||||
for pp in act.get('plot_points', []):
|
||||
beats_context.append(f" * {pp.get('point', 'Beat')}: {pp.get('description', '')}")
|
||||
|
||||
if not beats_context:
|
||||
beats_context = bp.get('plot_beats', [])
|
||||
@@ -206,13 +185,6 @@ def expand(events, pass_num, target_chapters, bp, folder):
|
||||
utils.log("ARCHITECT", f"Expansion pass {pass_num} | Current Beats: {len(events)} | Target Chaps: {target_chapters}")
|
||||
|
||||
beats_context = []
|
||||
if 'plot_outline' in bp and isinstance(bp['plot_outline'], dict):
|
||||
po = bp['plot_outline']
|
||||
if 'beats' in po:
|
||||
for act in po['beats']:
|
||||
beats_context.append(f"ACT {act.get('act', '?')}: {act.get('title', '')} - {act.get('summary', '')}")
|
||||
for pp in act.get('plot_points', []):
|
||||
beats_context.append(f" * {pp.get('point', 'Beat')}: {pp.get('description', '')}")
|
||||
|
||||
if not beats_context:
|
||||
beats_context = bp.get('plot_beats', [])
|
||||
@@ -565,7 +537,8 @@ def write_chapter(chap, bp, folder, prev_sum, tracking=None, prev_content=None):
|
||||
|
||||
prev_context_block = ""
|
||||
if prev_content:
|
||||
prev_context_block = f"\nPREVIOUS CHAPTER TEXT (For Tone & Continuity):\n{prev_content}\n"
|
||||
trunc_content = prev_content[-3000:] if len(prev_content) > 3000 else prev_content
|
||||
prev_context_block = f"\nPREVIOUS CHAPTER TEXT (For Tone & Continuity):\n{trunc_content}\n"
|
||||
|
||||
prompt = f"""
|
||||
Write Chapter {chap['chapter_number']}: {chap['title']}
|
||||
@@ -617,7 +590,10 @@ 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 = 9
|
||||
max_attempts = 3
|
||||
SCORE_AUTO_ACCEPT = 9
|
||||
SCORE_PASSING = 7
|
||||
|
||||
best_score = 0
|
||||
best_text = current_text
|
||||
past_critiques = []
|
||||
@@ -635,8 +611,8 @@ def write_chapter(chap, bp, folder, prev_sum, tracking=None, prev_content=None):
|
||||
|
||||
utils.log("WRITER", f" Score: {score}/10. Critique: {critique}")
|
||||
|
||||
if score >= 8:
|
||||
utils.log("WRITER", " Quality threshold met.")
|
||||
if score >= SCORE_AUTO_ACCEPT:
|
||||
utils.log("WRITER", " 🌟 Auto-Accept threshold met.")
|
||||
return current_text
|
||||
|
||||
if score > best_score:
|
||||
@@ -644,8 +620,12 @@ def write_chapter(chap, bp, folder, prev_sum, tracking=None, prev_content=None):
|
||||
best_text = current_text
|
||||
|
||||
if attempt == max_attempts:
|
||||
utils.log("WRITER", " Max attempts reached. Using best version.")
|
||||
return best_text
|
||||
if best_score >= SCORE_PASSING:
|
||||
utils.log("WRITER", f" ✅ Max attempts reached. Accepting best score ({best_score}).")
|
||||
return best_text
|
||||
else:
|
||||
utils.log("WRITER", f" ⚠️ Quality low ({best_score}/{SCORE_PASSING}) but max attempts reached. Proceeding.")
|
||||
return best_text
|
||||
|
||||
utils.log("WRITER", f" -> Refining Ch {chap['chapter_number']} based on feedback...")
|
||||
|
||||
@@ -692,8 +672,15 @@ def write_chapter(chap, bp, folder, prev_sum, tracking=None, prev_content=None):
|
||||
|
||||
def harvest_metadata(bp, folder, full_manuscript):
|
||||
utils.log("HARVESTER", "Scanning for new characters...")
|
||||
full_text = "\n".join([c['content'] for c in full_manuscript])[:50000]
|
||||
prompt = f"Identify new significant characters NOT in:\n{json.dumps(bp['characters'])}\nTEXT:\n{full_text}\nReturn JSON: {{'new_characters': [{{'name':'...', 'role':'...', 'description':'...'}}]}}"
|
||||
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'])}
|
||||
|
||||
TASK: Identify NEW significant characters that appear in the text but are missing from the list.
|
||||
RETURN JSON: {{'new_characters': [{{'name':'...', 'role':'...', 'description':'...'}}]}}
|
||||
"""
|
||||
try:
|
||||
response = ai.model_logic.generate_content(prompt)
|
||||
utils.log_usage(folder, "logic-pro", response.usage_metadata)
|
||||
@@ -722,7 +709,7 @@ def update_persona_sample(bp, folder):
|
||||
if not os.path.exists(config.PERSONAS_DIR): os.makedirs(config.PERSONAS_DIR)
|
||||
|
||||
meta = bp.get('book_metadata', {})
|
||||
safe_title = "".join([c for c in meta.get('title', 'book') if c.isalnum() or c=='_']).replace(" ", "_")[:20]
|
||||
safe_title = utils.sanitize_filename(meta.get('title', 'book'))[:20]
|
||||
timestamp = int(time.time())
|
||||
filename = f"sample_{safe_title}_{timestamp}.txt"
|
||||
filepath = os.path.join(config.PERSONAS_DIR, filename)
|
||||
@@ -781,4 +768,236 @@ def refine_bible(bible, instruction, folder):
|
||||
return new_data
|
||||
except Exception as e:
|
||||
utils.log("SYSTEM", f"Refinement failed: {e}")
|
||||
return None
|
||||
return None
|
||||
|
||||
def analyze_consistency(bp, manuscript, folder):
|
||||
utils.log("EDITOR", "Analyzing manuscript for continuity errors...")
|
||||
|
||||
if not manuscript: return {"issues": ["No manuscript found."], "score": 0}
|
||||
if not bp: return {"issues": ["No blueprint found."], "score": 0}
|
||||
|
||||
# Summarize chapters to save tokens (pass full text if small enough, but usually summaries are safer)
|
||||
chapter_summaries = []
|
||||
for ch in manuscript:
|
||||
text = ch.get('content', '')
|
||||
# Take first 1000 and last 1000 chars to capture setup and resolution of scenes
|
||||
excerpt = text[:1000] + "\n...\n" + text[-1000:] if len(text) > 2000 else text
|
||||
chapter_summaries.append(f"Ch {ch.get('num')}: {excerpt}")
|
||||
|
||||
context = "\n".join(chapter_summaries)
|
||||
|
||||
prompt = f"""
|
||||
Act as a Continuity Editor. Analyze this book summary for plot holes and inconsistencies.
|
||||
|
||||
CHARACTERS: {json.dumps(bp.get('characters', []))}
|
||||
|
||||
CHAPTER 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)
|
||||
"""
|
||||
try:
|
||||
response = ai.model_logic.generate_content(prompt)
|
||||
utils.log_usage(folder, "logic-pro", response.usage_metadata)
|
||||
return json.loads(utils.clean_json(response.text))
|
||||
except Exception as e:
|
||||
return {"issues": [f"Analysis failed: {e}"], "score": 0, "summary": "Error during analysis."}
|
||||
|
||||
def rewrite_chapter_content(bp, manuscript, chapter_num, instruction, folder):
|
||||
utils.log("WRITER", f"Rewriting Ch {chapter_num} with instruction: {instruction}")
|
||||
|
||||
# Find target chapter and previous context
|
||||
target_chap = next((c for c in manuscript if c['num'] == chapter_num), None)
|
||||
if not target_chap: return None
|
||||
|
||||
prev_text = ""
|
||||
|
||||
# Determine previous chapter logic
|
||||
prev_chap = None
|
||||
if isinstance(chapter_num, int):
|
||||
prev_chap = next((c for c in manuscript if c['num'] == chapter_num - 1), None)
|
||||
elif str(chapter_num).lower() == "epilogue":
|
||||
# Find the highest numbered chapter
|
||||
numbered_chaps = [c for c in manuscript if isinstance(c['num'], int)]
|
||||
if numbered_chaps:
|
||||
prev_chap = max(numbered_chaps, key=lambda x: x['num'])
|
||||
|
||||
if prev_chap:
|
||||
prev_text = prev_chap.get('content', '')[-3000:] # Last 3000 chars for context
|
||||
|
||||
meta = bp.get('book_metadata', {})
|
||||
|
||||
prompt = f"""
|
||||
Act as a Ghostwriter. Rewrite Chapter {chapter_num}: {target_chap.get('title', '')}
|
||||
|
||||
USER INSTRUCTION (PRIMARY DIRECTIVE):
|
||||
{instruction}
|
||||
|
||||
STORY CONTEXT:
|
||||
- Title: {meta.get('title')}
|
||||
- Genre: {meta.get('genre')}
|
||||
- Tone: {meta.get('style', {}).get('tone')}
|
||||
|
||||
PREVIOUS CHAPTER ENDING (Continuity):
|
||||
{prev_text}
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
try:
|
||||
response = ai.model_writer.generate_content(prompt)
|
||||
utils.log_usage(folder, "writer-flash", response.usage_metadata)
|
||||
return response.text
|
||||
except Exception as e:
|
||||
utils.log("WRITER", f"Rewrite failed: {e}")
|
||||
return None
|
||||
|
||||
def check_and_propagate(bp, manuscript, changed_chap_num, folder):
|
||||
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
|
||||
|
||||
original_change_context = current_context
|
||||
# Iterate subsequent chapters
|
||||
sorted_ms = sorted(manuscript, key=utils.chapter_sort_key)
|
||||
start_index = -1
|
||||
for i, c in enumerate(sorted_ms):
|
||||
if str(c['num']) == str(changed_chap_num):
|
||||
start_index = i
|
||||
break
|
||||
|
||||
if start_index == -1 or start_index == len(sorted_ms) - 1:
|
||||
return None
|
||||
|
||||
changes_made = False
|
||||
consecutive_no_changes = 0
|
||||
potential_impact_chapters = []
|
||||
|
||||
for i in range(start_index + 1, len(sorted_ms)):
|
||||
target_chap = sorted_ms[i]
|
||||
|
||||
# Optimization: If 2 chapters in a row didn't need changes, assume the ripple has stopped locally.
|
||||
# Perform Long-Range Scan to see if we need to jump ahead.
|
||||
if consecutive_no_changes >= 2:
|
||||
if target_chap['num'] not in potential_impact_chapters:
|
||||
# Check if we have pending future flags
|
||||
future_flags = [n for n in potential_impact_chapters if isinstance(n, int) and isinstance(target_chap['num'], int) and n > target_chap['num']]
|
||||
|
||||
if not future_flags:
|
||||
# No pending flags. Scan remaining chapters.
|
||||
remaining_chaps = sorted_ms[i:]
|
||||
if not remaining_chaps: break
|
||||
|
||||
utils.log("WRITER", " -> Short-term ripple dissipated. Scanning remaining chapters for long-range impacts...")
|
||||
|
||||
chapter_summaries = []
|
||||
for rc in remaining_chaps:
|
||||
text = rc.get('content', '')
|
||||
excerpt = text[:500] + "\n...\n" + text[-500:] if len(text) > 1000 else text
|
||||
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.
|
||||
|
||||
ORIGINAL CHANGE CONTEXT:
|
||||
{original_change_context}
|
||||
|
||||
REMAINING CHAPTERS:
|
||||
{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 [].
|
||||
"""
|
||||
|
||||
try:
|
||||
resp = ai.model_logic.generate_content(scan_prompt)
|
||||
potential_impact_chapters = json.loads(utils.clean_json(resp.text))
|
||||
if not isinstance(potential_impact_chapters, list): potential_impact_chapters = []
|
||||
# Ensure integers
|
||||
potential_impact_chapters = [int(x) for x in potential_impact_chapters if str(x).isdigit()]
|
||||
except Exception as e:
|
||||
utils.log("WRITER", f" -> Scan failed: {e}. Stopping.")
|
||||
break
|
||||
|
||||
if not potential_impact_chapters:
|
||||
utils.log("WRITER", " -> No long-range impacts detected. Stopping.")
|
||||
break
|
||||
else:
|
||||
utils.log("WRITER", f" -> Detected potential impact in chapters: {potential_impact_chapters}")
|
||||
|
||||
# If current chapter is still not in the list, skip it
|
||||
# Safety: Always check non-integer chapters (Prologue/Epilogue) to be safe
|
||||
if isinstance(target_chap['num'], int) and target_chap['num'] not in potential_impact_chapters:
|
||||
utils.log("WRITER", f" -> Skipping Ch {target_chap['num']} (Not flagged).")
|
||||
continue
|
||||
|
||||
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}
|
||||
|
||||
CURRENT TEXT of Ch {target_chap['num']}:
|
||||
{target_chap['content'][:5000]}... (truncated)
|
||||
|
||||
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".
|
||||
"""
|
||||
|
||||
try:
|
||||
response = ai.model_writer.generate_content(prompt)
|
||||
text = response.text.strip()
|
||||
|
||||
if "NO_CHANGE" in text[:20] and len(text) < 100:
|
||||
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
|
||||
|
||||
# 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}")
|
||||
|
||||
return manuscript if changes_made else None
|
||||
Reference in New Issue
Block a user