diff --git a/ai_blueprint.md b/ai_blueprint.md index 2348d0d..f32f9a9 100644 --- a/ai_blueprint.md +++ b/ai_blueprint.md @@ -1,4 +1,4 @@ -# AI Context Optimization Blueprint (v2.6) +# AI Context Optimization Blueprint (v2.7) This blueprint outlines architectural improvements for how AI context is managed during the writing process. The goal is to provide the AI (Claude/Gemini) with **better, highly-targeted context upfront**, which will dramatically improve first-draft quality and reduce the reliance on expensive, time-consuming quality checks and rewrites (currently up to 5 attempts). @@ -115,6 +115,15 @@ The `templates/consistency_report.html` page displays issues found in the manusc 1. ✅ **Frontend Action:** Added "Redo Book" form to `templates/consistency_report.html` footer with a text input for the revision instruction and a confirmation prompt on submit. *(Implemented v2.6)* 2. ✅ **Backend Route:** Added `/project//revise_book/` route in `web/routes/run.py`. Route creates a new `Run` record and queues `generate_book_task` with the user's instruction as `feedback` and `source_run_id` pointing to the original run. The existing bible refinement logic in `generate_book_task` applies the instruction to the bible before regenerating. *(Implemented v2.6)* +## 11. Series Continuity & Book Number Awareness (v2.7) + +**Current Problem:** +The system generates books for a series, but the prompts in `story/planner.py` (specifically `enrich` and `plan_structure`) and the writing prompts do not explicitly pass the `series_metadata` (such as `is_series`, `series_title`, `book_number`, and `total_books`) to the LLM. The AI doesn't know if it's generating Book 1, Book 2, or Book 3, leading to inconsistent pacing and continuity across a series. + +**Solution & Implementation Plan:** +1. ✅ **Planner Prompts Update:** Modified `enrich()` and `plan_structure()` in `story/planner.py` to extract `bp.get('series_metadata', {})` and inject a `SERIES_CONTEXT` block — "This is Book X of Y in the Z series" with position-aware guidance (Book 1 = establish, middle books = escalate, final book = resolve) — into the prompt when `is_series` is true. *(Implemented v2.7)* +2. ✅ **Writer Prompts Update:** `story/writer.py` `write_chapter()` builds and injects the same `SERIES_CONTEXT` block into the chapter writing prompt and passes it as `series_context` to `evaluate_chapter_quality()` in `story/editor.py`. `editor.py` `evaluate_chapter_quality()` now accepts an optional `series_context` parameter and injects it into the evaluation METADATA so the editor scores arcs relative to the book's position in the series. *(Implemented v2.7)* + ## Summary of Actionable Changes for Implementation Mode: 1. ✅ Modify `writer.py` to filter `chars_for_writer` based on characters named in `beats`. *(Implemented in v1.5.0)* 2. ✅ Modify `writer.py` `prev_content` logic to extract the *tail* of the chapter, not a blind slice. *(Implemented in v1.5.0 via `utils.truncate_to_tokens` tail logic)* @@ -126,3 +135,4 @@ The `templates/consistency_report.html` page displays issues found in the manusc 8. ✅ **(v2.5)** Lore & Location RAG-Lite: `update_lore_index` in `bible_tracker.py`, `tracking_lore.json`, lore retrieval in `writer.py`, wired in `engine.py`. *(Implemented v2.5)* 9. ✅ **(v2.5)** Structured Story State (Thread Tracking): new `story/state.py`, `story_state.json`, structured prompt context replacing raw summary blob in `engine.py`. *(Implemented v2.5)* 10. ✅ **(v2.6)** "Redo Book" form in `consistency_report.html` + `revise_book` route in `run.py` that creates a new run with the instruction applied as bible feedback. *(Implemented v2.6)* +11. ✅ **(v2.7)** Series Continuity Fix: `series_metadata` (is_series, series_title, book_number, total_books) injected as `SERIES_CONTEXT` into `story/planner.py` (`enrich`, `plan_structure`), `story/writer.py` (`write_chapter`), and `story/editor.py` (`evaluate_chapter_quality`) prompts with position-aware guidance per book number. *(Implemented v2.7)* diff --git a/story/editor.py b/story/editor.py index 3c6db5d..f33aa9d 100644 --- a/story/editor.py +++ b/story/editor.py @@ -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} diff --git a/story/planner.py b/story/planner.py index 242dd72..7686684 100644 --- a/story/planner.py +++ b/story/planner.py @@ -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)} diff --git a/story/writer.py b/story/writer.py index 2622907..aeb732a 100644 --- a/story/writer.py +++ b/story/writer.py @@ -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}")