Blueprint v1.5.0: AI Context Optimization — Dynamic Characters & Scene State

- writer.py: Dynamic character injection — only POV + beat-named characters
  are sent to the writer prompt, eliminating token waste and hallucinations
  from characters unrelated to the current scene.
- writer.py: Smart tail truncation — prev_content trimmed to last 1,000 tokens
  (the actual chapter ending) instead of a blind 2,000-token head slice,
  preserving the exact hand-off point for continuity.
- writer.py: Scene state injected into char_visuals — current_location,
  time_of_day, and held_items now surfaced per relevant character in prompt.
- bible_tracker.py: update_tracking expanded to record current_location,
  time_of_day, and held_items per character after each chapter.
- core/config.py: VERSION bumped 1.4.0 → 1.5.0.
- README.md: Story Generation section and tracking_characters.json schema
  updated to document new context optimization features.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-21 00:01:47 -05:00
parent fd4ce634d4
commit f71a04c03c
4 changed files with 42 additions and 11 deletions

View File

@@ -112,6 +112,9 @@ Open `http://localhost:5000`.
- **Series Continuity:** When generating Book 2+, carries forward character visual tracking, established relationships, plot threads, and a cumulative "Story So Far" summary. - **Series Continuity:** When generating Book 2+, carries forward character visual tracking, established relationships, plot threads, and a cumulative "Story So Far" summary.
- **Persona Refinement Loop:** Every 5 chapters, analyzes actual written text to refine the author persona model, maintaining stylistic consistency throughout the book. - **Persona Refinement Loop:** Every 5 chapters, analyzes actual written text to refine the author persona model, maintaining stylistic consistency throughout the book.
- **Consistency Checker (`editor.py`):** Scores chapters on 8 rubrics (engagement, voice, sensory detail, scene execution, etc.) and flags AI-isms ("tapestry", "palpable tension") and weak filter verbs ("felt", "realized"). - **Consistency Checker (`editor.py`):** Scores chapters on 8 rubrics (engagement, voice, sensory detail, scene execution, etc.) and flags AI-isms ("tapestry", "palpable tension") and weak filter verbs ("felt", "realized").
- **Dynamic Character Injection (`writer.py`):** Only injects characters explicitly named in the chapter's `scene_beats` plus the POV character into the writer prompt. Eliminates token waste from unused characters and reduces hallucinated appearances.
- **Smart Context Tail (`writer.py`):** Extracts the final ~1,000 tokens of the previous chapter (the actual ending) rather than blindly truncating from the front. Ensures the hand-off point — where characters are standing and what was last said — is always preserved.
- **Stateful Scene Tracking (`bible_tracker.py`):** After each chapter, the tracker records each character's `current_location`, `time_of_day`, and `held_items` in addition to appearance and events. This scene state is injected into subsequent chapter prompts so the writer knows exactly where characters are, what time it is, and what they're carrying.
### Marketing Assets (`marketing/`) ### Marketing Assets (`marketing/`)
- **Cover Art:** Generates a visual prompt from book themes and tracking data, then calls Imagen (Gemini or Vertex AI) to produce the cover. Evaluates image quality with multimodal AI critique before accepting. - **Cover Art:** Generates a visual prompt from book themes and tracking data, then calls Imagen (Gemini or Vertex AI) to produce the cover. Evaluates image quality with multimodal AI critique before accepting.
@@ -316,7 +319,10 @@ data/
"descriptors": ["Blue eyes", "Tall"], "descriptors": ["Blue eyes", "Tall"],
"likes_dislikes": ["Loves coffee"], "likes_dislikes": ["Loves coffee"],
"last_worn": "Red dress (Ch 4)", "last_worn": "Red dress (Ch 4)",
"major_events": ["Injured leg in Ch 2"] "major_events": ["Injured leg in Ch 2"],
"current_location": "The King's Throne Room",
"time_of_day": "Late afternoon",
"held_items": ["Iron sword", "Stolen ledger"]
} }
} }
``` ```

View File

@@ -65,4 +65,4 @@ LENGTH_DEFINITIONS = {
} }
# --- SYSTEM --- # --- SYSTEM ---
VERSION = "1.4.0" VERSION = "1.5.0"

View File

@@ -70,12 +70,15 @@ def update_tracking(folder, chapter_num, chapter_text, current_tracking):
OPERATIONS: OPERATIONS:
1. EVENTS: Append 1-3 key plot points to 'events'. 1. EVENTS: Append 1-3 key plot points to 'events'.
2. CHARACTERS: Update 'descriptors', 'likes_dislikes', 'speech_style', 'last_worn', 'major_events'. 2. CHARACTERS: Update 'descriptors', 'likes_dislikes', 'speech_style', 'last_worn', 'major_events', 'current_location', 'time_of_day', 'held_items'.
- "descriptors": List of strings. Add PERMANENT physical traits (height, hair, eyes), specific items (jewelry, weapons). Avoid duplicates. - "descriptors": List of strings. Add PERMANENT physical traits (height, hair, eyes), specific items (jewelry, weapons). Avoid duplicates.
- "likes_dislikes": List of strings. Add specific preferences, likes, or dislikes mentioned (e.g., "Hates coffee", "Loves jazz"). - "likes_dislikes": List of strings. Add specific preferences, likes, or dislikes mentioned (e.g., "Hates coffee", "Loves jazz").
- "speech_style": String. Describe how they speak (e.g. "Formal, no contractions", "Uses slang", "Stutters", "Short sentences"). - "speech_style": String. Describe how they speak (e.g. "Formal, no contractions", "Uses slang", "Stutters", "Short sentences").
- "last_worn": String. Update if specific clothing is described. IMPORTANT: If a significant time jump occurred (e.g. next day) and no new clothing is described, reset this to "Unknown". - "last_worn": String. Update if specific clothing is described. IMPORTANT: If a significant time jump occurred (e.g. next day) and no new clothing is described, reset this to "Unknown".
- "major_events": List of strings. Log significant life-altering events occurring in THIS chapter (e.g. "Lost an arm", "Married", "Betrayed by X"). - "major_events": List of strings. Log significant life-altering events occurring in THIS chapter (e.g. "Lost an arm", "Married", "Betrayed by X").
- "current_location": String. The character's physical location at the END of this chapter (e.g., "The King's Throne Room", "Aboard the Nighthawk ship"). Update whenever the character moves.
- "time_of_day": String. The approximate time of day at the END of this chapter (e.g., "Dawn", "Late afternoon", "Midnight"). Reset to "Unknown" if unclear.
- "held_items": List of strings. Items the character is actively carrying or holding at chapter end (e.g., "Iron sword", "Stolen ledger"). Remove items they have dropped or given away.
3. WARNINGS: Append new 'content_warnings'. 3. WARNINGS: Append new 'content_warnings'.
OUTPUT_FORMAT (JSON): Return the updated tracking object structure. OUTPUT_FORMAT (JSON): Return the updated tracking object structure.

View File

@@ -46,10 +46,30 @@ def write_chapter(chap, bp, folder, prev_sum, tracking=None, prev_content=None,
if samples: if samples:
persona_info += "\nWRITING STYLE SAMPLES:\n" + "\n".join(samples) persona_info += "\nWRITING STYLE SAMPLES:\n" + "\n".join(samples)
# Only inject characters named in the chapter beats + the POV character
beats_text = " ".join(str(b) for b in chap.get('beats', []))
pov_lower = pov_char.lower() if pov_char else ""
chars_for_writer = [
{"name": c.get("name"), "role": c.get("role"), "description": c.get("description", "")}
for c in bp.get('characters', [])
if c.get("name") and (
c["name"].lower() in beats_text.lower() or
(pov_lower and c["name"].lower() == pov_lower)
)
]
if not chars_for_writer:
chars_for_writer = [
{"name": c.get("name"), "role": c.get("role"), "description": c.get("description", "")}
for c in bp.get('characters', [])
]
relevant_names = {c["name"] for c in chars_for_writer}
char_visuals = "" char_visuals = ""
if tracking and 'characters' in tracking: if tracking and 'characters' in tracking:
char_visuals = "\nCHARACTER TRACKING (Visuals & Preferences):\n" char_visuals = "\nCHARACTER TRACKING (Visuals, State & Scene Position):\n"
for name, data in tracking['characters'].items(): for name, data in tracking['characters'].items():
if name not in relevant_names:
continue
desc = ", ".join(data.get('descriptors', [])) desc = ", ".join(data.get('descriptors', []))
likes = ", ".join(data.get('likes_dislikes', [])) likes = ", ".join(data.get('likes_dislikes', []))
speech = data.get('speech_style', 'Unknown') speech = data.get('speech_style', 'Unknown')
@@ -62,6 +82,13 @@ def write_chapter(chap, bp, folder, prev_sum, tracking=None, prev_content=None,
if worn and worn != 'Unknown': if worn and worn != 'Unknown':
char_visuals += f" * Last Worn: {worn} (NOTE: Only relevant if scene is continuous from previous chapter)\n" char_visuals += f" * Last Worn: {worn} (NOTE: Only relevant if scene is continuous from previous chapter)\n"
location = data.get('current_location', '')
items = data.get('held_items', [])
if location:
char_visuals += f" * Current Location: {location}\n"
if items:
char_visuals += f" * Held Items: {', '.join(items)}\n"
style_block = "\n".join([f"- {k.replace('_', ' ').title()}: {v}" for k, v in style.items() if isinstance(v, (str, int, float))]) style_block = "\n".join([f"- {k.replace('_', ' ').title()}: {v}" for k, v in style.items() if isinstance(v, (str, int, float))])
if 'tropes' in style and isinstance(style['tropes'], list): if 'tropes' in style and isinstance(style['tropes'], list):
style_block += f"\n- Tropes: {', '.join(style['tropes'])}" style_block += f"\n- Tropes: {', '.join(style['tropes'])}"
@@ -71,13 +98,8 @@ def write_chapter(chap, bp, folder, prev_sum, tracking=None, prev_content=None,
prev_context_block = "" prev_context_block = ""
if prev_content: if prev_content:
trunc_content = utils.truncate_to_tokens(prev_content, 2000) trunc_content = utils.truncate_to_tokens(prev_content, 1000)
prev_context_block = f"\nPREVIOUS CHAPTER TEXT (For Tone & Continuity):\n{trunc_content}\n" prev_context_block = f"\nPREVIOUS CHAPTER TEXT (Last ~1000 Tokens — For Immediate Continuity):\n{trunc_content}\n"
chars_for_writer = [
{"name": c.get("name"), "role": c.get("role"), "description": c.get("description", "")}
for c in bp.get('characters', [])
]
total_chapters = ls.get('chapters', '?') total_chapters = ls.get('chapters', '?')
prompt = f""" prompt = f"""