Final changes and update

This commit is contained in:
2026-02-04 20:19:07 -05:00
parent 6e7ff0ae1d
commit 9f8f094564
21 changed files with 1816 additions and 645 deletions

View File

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