diff --git a/README.md b/README.md index 82eca44..a6e2305 100644 --- a/README.md +++ b/README.md @@ -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. - **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"). +- **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/`) - **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"], "likes_dislikes": ["Loves coffee"], "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"] } } ``` diff --git a/core/config.py b/core/config.py index 63b5f1b..f61ffe7 100644 --- a/core/config.py +++ b/core/config.py @@ -65,4 +65,4 @@ LENGTH_DEFINITIONS = { } # --- SYSTEM --- -VERSION = "1.4.0" +VERSION = "1.5.0" diff --git a/story/bible_tracker.py b/story/bible_tracker.py index 2f48ded..ced46da 100644 --- a/story/bible_tracker.py +++ b/story/bible_tracker.py @@ -70,12 +70,15 @@ def update_tracking(folder, chapter_num, chapter_text, current_tracking): OPERATIONS: 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. - "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"). - "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"). + - "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'. OUTPUT_FORMAT (JSON): Return the updated tracking object structure. diff --git a/story/writer.py b/story/writer.py index 4145d3a..5b6b1c5 100644 --- a/story/writer.py +++ b/story/writer.py @@ -46,10 +46,30 @@ def write_chapter(chap, bp, folder, prev_sum, tracking=None, prev_content=None, if 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 = "" 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(): + if name not in relevant_names: + continue desc = ", ".join(data.get('descriptors', [])) likes = ", ".join(data.get('likes_dislikes', [])) 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': 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))]) if 'tropes' in style and isinstance(style['tropes'], list): 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 = "" if prev_content: - trunc_content = utils.truncate_to_tokens(prev_content, 2000) - prev_context_block = f"\nPREVIOUS CHAPTER TEXT (For Tone & 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', []) - ] + trunc_content = utils.truncate_to_tokens(prev_content, 1000) + prev_context_block = f"\nPREVIOUS CHAPTER TEXT (Last ~1000 Tokens — For Immediate Continuity):\n{trunc_content}\n" total_chapters = ls.get('chapters', '?') prompt = f"""