v1.4.0: Organic writing, speed, and log improvements
Organic book quality: - write_chapter: strip key_events spoilers from character context so the writer doesn't know planned future events when writing early chapters - write_chapter: added next_chapter_hint — seeds anticipation for the next scene in the final paragraphs of each chapter for natural story flow - write_chapter: added DIALOGUE VOICE instruction referencing CHARACTER TRACKING speech styles so every character sounds distinctly different - Lowered SCORE_AUTO_ACCEPT 9→8 to stop over-refining already-professional drafts Speed improvements: - check_pacing: reduced from every chapter to every other chapter (~50% fewer calls) - refine_persona: reduced from every 3 to every 5 chapters (~40% fewer calls) - Resume summary rebuild: uses first + last-4 chapters instead of all chapters to avoid massive prompts when resuming mid-book - Summary context sent to writer capped at 8000 chars (most-recent events) - update_tracking text cap lowered 500000→20000 (covers any realistic chapter) Logging and progress bars: - Progress bar updates at chapter START, not just after completion - Chapter banner logged before each write so the log shows which chapter is active - Word count logged after first draft (e.g. "Draft: 2,341 words (target: ~2200)") - Word count added to chapter completion TIMING line - Pacing check now logs "Pacing OK" with reason when no intervention needed - utils: added log_banner() helper for phase separator lines UI: - run_details.html: log lines are now phase-coloured (WRITER=cyan, ARCHITECT=green, TIMING=gray, SYSTEM=yellow, TRACKER=purple, RESUME=orange, etc.) - Status bar shows current active phase (e.g. "Status: Running — WRITER") Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -411,7 +411,7 @@ def update_tracking(folder, chapter_num, chapter_text, current_tracking):
|
||||
{json.dumps(current_tracking)}
|
||||
|
||||
NEW_TEXT:
|
||||
{chapter_text[:500000]}
|
||||
{chapter_text[:20000]}
|
||||
|
||||
OPERATIONS:
|
||||
1. EVENTS: Append 1-3 key plot points to 'events'.
|
||||
@@ -594,7 +594,7 @@ def refine_persona(bp, text, folder):
|
||||
except: pass
|
||||
return ad
|
||||
|
||||
def write_chapter(chap, bp, folder, prev_sum, tracking=None, prev_content=None):
|
||||
def write_chapter(chap, bp, folder, prev_sum, tracking=None, prev_content=None, next_chapter_hint=""):
|
||||
pacing = chap.get('pacing', 'Standard')
|
||||
est_words = chap.get('estimated_words', 'Flexible')
|
||||
utils.log("WRITER", f"Drafting Ch {chap['chapter_number']} ({pacing} | ~{est_words} words): {chap['title']}")
|
||||
@@ -662,6 +662,13 @@ def write_chapter(chap, bp, folder, prev_sum, tracking=None, prev_content=None):
|
||||
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"
|
||||
|
||||
# Strip future planning notes (key_events) from character context — the writer
|
||||
# should not know what is *planned* to happen; only name, role, and description.
|
||||
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', '?')
|
||||
prompt = f"""
|
||||
ROLE: Fiction Writer
|
||||
@@ -705,7 +712,9 @@ def write_chapter(chap, bp, folder, prev_sum, tracking=None, prev_content=None):
|
||||
- CHARACTER INTERACTIONS: If characters are meeting for the first time in the summary, treat them as strangers.
|
||||
- SENTENCE VARIETY: Avoid repetitive sentence structures (e.g. starting multiple sentences with "He" or "She"). Vary sentence length to create rhythm.
|
||||
- GENRE CONSISTENCY: Ensure all introductions of characters, places, items, or actions are strictly appropriate for the {genre} genre. Avoid anachronisms or tonal clashes.
|
||||
|
||||
- DIALOGUE VOICE: Every character speaks with their own distinct voice (see CHARACTER TRACKING for speech styles). No two characters may sound the same. Vary sentence length, vocabulary, and register per character.
|
||||
- CHAPTER HOOK: End this chapter with unresolved tension — a decision pending, a threat imminent, or a question unanswered.{f" Seed subtle anticipation for the next scene: '{next_chapter_hint}'." if next_chapter_hint else " Do not neatly resolve all threads."}
|
||||
|
||||
QUALITY_CRITERIA:
|
||||
1. ENGAGEMENT & TENSION: Grip the reader. Ensure conflict/tension in every scene.
|
||||
2. SCENE EXECUTION: Flesh out the middle. Avoid summarizing key moments.
|
||||
@@ -724,7 +733,7 @@ def write_chapter(chap, bp, folder, prev_sum, tracking=None, prev_content=None):
|
||||
CONTEXT:
|
||||
- STORY_SO_FAR: {prev_sum}
|
||||
{prev_context_block}
|
||||
- CHARACTERS: {json.dumps(bp['characters'])}
|
||||
- CHARACTERS: {json.dumps(chars_for_writer)}
|
||||
{char_visuals}
|
||||
- SCENE_BEATS: {json.dumps(chap['beats'])}
|
||||
|
||||
@@ -735,13 +744,15 @@ def write_chapter(chap, bp, folder, prev_sum, tracking=None, prev_content=None):
|
||||
resp_draft = ai.model_writer.generate_content(prompt)
|
||||
utils.log_usage(folder, ai.model_writer.name, resp_draft.usage_metadata)
|
||||
current_text = resp_draft.text
|
||||
draft_words = len(current_text.split()) if current_text else 0
|
||||
utils.log("WRITER", f" -> Draft: {draft_words:,} words (target: ~{est_words})")
|
||||
except Exception as e:
|
||||
utils.log("WRITER", f"⚠️ Failed Ch {chap['chapter_number']}: {e}")
|
||||
return f"## Chapter {chap['chapter_number']} Failed\n\nError: {e}"
|
||||
|
||||
# Refinement Loop
|
||||
max_attempts = 5
|
||||
SCORE_AUTO_ACCEPT = 9
|
||||
SCORE_AUTO_ACCEPT = 8 # 8 = professional quality; no marginal gain from extra refinement
|
||||
SCORE_PASSING = 7
|
||||
SCORE_REWRITE_THRESHOLD = 6
|
||||
|
||||
|
||||
@@ -71,7 +71,11 @@ def get_sorted_book_folders(run_dir):
|
||||
return sorted(subdirs, key=sort_key)
|
||||
|
||||
# --- SHARED UTILS ---
|
||||
def log(phase, msg):
|
||||
def log_banner(phase, title):
|
||||
"""Log a visually distinct phase separator line."""
|
||||
log(phase, f"{'─' * 18} {title} {'─' * 18}")
|
||||
|
||||
def log(phase, msg):
|
||||
timestamp = datetime.datetime.now().strftime('%H:%M:%S')
|
||||
line = f"[{timestamp}] {phase:<15} | {msg}"
|
||||
print(line)
|
||||
|
||||
Reference in New Issue
Block a user