Auto-commit: v2.7 Series Continuity & Book Number Awareness

- story/planner.py: enrich() and plan_structure() now extract series_metadata
  and inject a SERIES_CONTEXT block (Book X of Y in series Z, with position-aware
  guidance) into prompts when is_series is true.
- story/writer.py: write_chapter() builds and injects the same SERIES_CONTEXT
  into the chapter draft prompt; passes series_context to evaluate_chapter_quality().
- story/editor.py: evaluate_chapter_quality() accepts optional series_context
  parameter and injects it into METADATA so arc pacing is evaluated relative to
  the book's position in the series.
- ai_blueprint.md: Section 11 marked complete (v2.7), summary updated.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-21 01:51:35 -05:00
parent 83a6a4315b
commit d75186cb29
4 changed files with 56 additions and 7 deletions

View File

@@ -5,7 +5,7 @@ from ai import models as ai_models
from story.style_persona import get_style_guidelines
def evaluate_chapter_quality(text, chapter_title, genre, model, folder):
def evaluate_chapter_quality(text, chapter_title, genre, model, folder, series_context=""):
guidelines = get_style_guidelines()
ai_isms = "', '".join(guidelines['ai_isms'])
fw_examples = ", ".join([f"'He {w}'" for w in guidelines['filter_words'][:5]])
@@ -15,13 +15,15 @@ def evaluate_chapter_quality(text, chapter_title, genre, model, folder):
max_sugg = min_sugg + 2
suggestion_range = f"{min_sugg}-{max_sugg}"
series_line = f"\n - {series_context}" if series_context else ""
prompt = f"""
ROLE: Senior Literary Editor
TASK: Critique chapter draft. Apply STRICT scoring — do not inflate scores.
METADATA:
- TITLE: {chapter_title}
- GENRE: {genre}
- GENRE: {genre}{series_line}
PROHIBITED_PATTERNS:
- AI_ISMS: {ai_isms}

View File

@@ -12,13 +12,26 @@ def enrich(bp, folder, context=""):
if 'characters' not in bp: bp['characters'] = []
if 'plot_beats' not in bp: bp['plot_beats'] = []
series_meta = bp.get('series_metadata', {})
series_block = ""
if series_meta.get('is_series'):
series_title = series_meta.get('series_title', 'this series')
book_num = series_meta.get('book_number', '?')
total_books = series_meta.get('total_books', '?')
series_block = (
f"\n - SERIES_CONTEXT: This is Book {book_num} of {total_books} in the '{series_title}' series. "
f"Pace character arcs and plot resolution accordingly. "
f"Book {book_num} of {total_books} should reflect its position: "
f"{'establish the world and core characters' if str(book_num) == '1' else 'escalate stakes and deepen arcs' if str(book_num) != str(total_books) else 'resolve all major threads with a satisfying conclusion'}."
)
prompt = f"""
ROLE: Creative Director
TASK: Create a comprehensive Book Bible from the user description.
INPUT DATA:
- USER_DESCRIPTION: "{bp.get('manual_instruction', 'A generic story')}"
- CONTEXT (Sequel): {context}
- CONTEXT (Sequel): {context}{series_block}
STEPS:
1. Generate a catchy Title.
@@ -96,6 +109,18 @@ def plan_structure(bp, folder):
target_words = bp.get('length_settings', {}).get('words', 'flexible')
chars_summary = [{"name": c.get("name"), "role": c.get("role")} for c in bp.get('characters', [])]
series_meta = bp.get('series_metadata', {})
series_block = ""
if series_meta.get('is_series'):
series_title = series_meta.get('series_title', 'this series')
book_num = series_meta.get('book_number', '?')
total_books = series_meta.get('total_books', '?')
series_block = (
f"\n - SERIES_CONTEXT: This is Book {book_num} of {total_books} in the '{series_title}' series. "
f"Structure the arc to fit its position in the series: "
f"{'introduce all major characters and the central conflict; leave threads open for future books' if str(book_num) == '1' else 'deepen existing character arcs and escalate the overarching conflict; do not resolve the series-level stakes' if str(book_num) != str(total_books) else 'resolve all series-level threads; provide a satisfying conclusion for every major character arc'}."
)
prompt = f"""
ROLE: Story Architect
TASK: Create a detailed structural event outline for a {target_chapters}-chapter book.
@@ -105,7 +130,7 @@ def plan_structure(bp, folder):
- GENRE: {bp.get('book_metadata', {}).get('genre', 'Fiction')}
- TARGET_CHAPTERS: {target_chapters}
- TARGET_WORDS: {target_words}
- STRUCTURE: {structure_type}
- STRUCTURE: {structure_type}{series_block}
CHARACTERS: {json.dumps(chars_summary)}

View File

@@ -223,6 +223,18 @@ def write_chapter(chap, bp, folder, prev_sum, tracking=None, prev_content=None,
genre_mandates = get_genre_instructions(genre)
series_meta = bp.get('series_metadata', {})
series_block = ""
if series_meta.get('is_series'):
series_title = series_meta.get('series_title', 'this series')
book_num = series_meta.get('book_number', '?')
total_books = series_meta.get('total_books', '?')
series_block = (
f"\n - SERIES_CONTEXT: This is Book {book_num} of {total_books} in the '{series_title}' series. "
f"Pace character arcs and emotional resolution to reflect this book's position in the series: "
f"{'establish foundations, plant seeds, avoid premature resolution of series-level stakes' if str(book_num) == '1' else 'escalate the overarching conflict, deepen character arcs, end on a compelling hook that carries into the next book' if str(book_num) != str(total_books) else 'resolve all major character arcs and series-level conflicts with earned, satisfying payoffs'}."
)
total_chapters = ls.get('chapters', '?')
prompt = f"""
ROLE: Fiction Writer
@@ -234,7 +246,7 @@ def write_chapter(chap, bp, folder, prev_sum, tracking=None, prev_content=None,
- POSITION: Chapter {chap['chapter_number']} of {total_chapters} — calibrate narrative tension accordingly (early = setup/intrigue, middle = escalation, final third = payoff/climax)
- PACING: {pacing} — see PACING_GUIDE below
- TARGET_WORDS: ~{est_words} (write to this length; do not summarise to save space)
- POV: {pov_char if pov_char else 'Protagonist'}
- POV: {pov_char if pov_char else 'Protagonist'}{series_block}
PACING_GUIDE:
- 'Very Fast': Pure action/dialogue. Minimal description. Short punchy paragraphs.
@@ -326,7 +338,7 @@ def write_chapter(chap, bp, folder, prev_sum, tracking=None, prev_content=None,
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'], meta.get('genre', 'Fiction'), ai_models.model_writer, folder)
score, critique = evaluate_chapter_quality(current_text, chap['title'], meta.get('genre', 'Fiction'), ai_models.model_writer, folder, series_context=series_block.strip())
past_critiques.append(f"Attempt {attempt}: {critique}")