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:
2026-02-20 10:59:08 -05:00
parent 958a6d0ea0
commit edabc4d4fa
5 changed files with 105 additions and 29 deletions

37
main.py
View File

@@ -97,10 +97,11 @@ def process_book(bp, folder, context="", resume=False, interactive=False):
summary = "The story begins."
if ms:
# Generate summary from ALL written chapters to maintain continuity
utils.log("RESUME", "Rebuilding 'Story So Far' from existing manuscript...")
try:
combined_text = "\n".join([f"Chapter {c['num']}: {c['content']}" for c in ms])
# Efficient rebuild: first chapter (setup) + last 4 (recent events) avoids huge prompts
utils.log("RESUME", f"Rebuilding story context from {len(ms)} existing chapters...")
try:
selected = ms[:1] + ms[-4:] if len(ms) > 5 else ms
combined_text = "\n".join([f"Chapter {c['num']}: {c['content'][:3000]}" for c in selected])
resp_sum = ai.model_writer.generate_content(f"""
ROLE: Series Historian
TASK: Create a cumulative 'Story So Far' summary.
@@ -133,13 +134,20 @@ def process_book(bp, folder, context="", resume=False, interactive=False):
if any(str(c.get('num')) == str(ch['chapter_number']) for c in ms):
i += 1
continue
# Progress Banner — update bar and log chapter header before writing begins
utils.update_progress(15 + int((i / len(chapters)) * 75))
utils.log_banner("WRITER", f"Chapter {ch['chapter_number']}/{len(chapters)}: {ch['title']}")
# Pass previous chapter content for continuity if available
prev_content = ms[-1]['content'] if ms else None
while True:
try:
txt = story.write_chapter(ch, bp, folder, summary, tracking, prev_content)
# Cap summary to most-recent 8000 chars; pass next chapter title as hook hint
summary_ctx = summary[-8000:] if len(summary) > 8000 else summary
next_hint = chapters[i+1]['title'] if i + 1 < len(chapters) else ""
txt = story.write_chapter(ch, bp, folder, summary_ctx, tracking, prev_content, next_chapter_hint=next_hint)
except Exception as e:
utils.log("SYSTEM", f"Chapter generation failed: {e}")
if interactive:
@@ -156,8 +164,8 @@ def process_book(bp, folder, context="", resume=False, interactive=False):
else:
break
# Refine Persona to match the actual output (Consistency Loop)
if (i == 0 or i % 3 == 0) and txt:
# Refine Persona to match the actual output (every 5 chapters to save API calls)
if (i == 0 or i % 5 == 0) and txt:
bp['book_metadata']['author_details'] = story.refine_persona(bp, txt, folder)
with open(bp_path, "w") as f: json.dump(bp, f, indent=2)
@@ -207,9 +215,9 @@ def process_book(bp, folder, context="", resume=False, interactive=False):
with open(chars_track_path, "w") as f: json.dump(tracking['characters'], f, indent=2)
with open(warn_track_path, "w") as f: json.dump(tracking.get('content_warnings', []), f, indent=2)
# --- DYNAMIC PACING CHECK ---
# --- DYNAMIC PACING CHECK (every other chapter to halve API overhead) ---
remaining = chapters[i+1:]
if remaining:
if remaining and len(remaining) >= 2 and i % 2 == 1:
pacing = story.check_pacing(bp, summary, txt, ch, remaining, folder)
if pacing and pacing.get('status') == 'add_bridge':
new_data = pacing.get('new_chapter', {})
@@ -237,10 +245,12 @@ def process_book(bp, folder, context="", resume=False, interactive=False):
removed = chapters.pop(i+1)
# Renumber subsequent chapters
for k in range(i+1, len(chapters)): chapters[k]['chapter_number'] = k + 1
with open(chapters_path, "w") as f: json.dump(chapters, f, indent=2)
utils.log("ARCHITECT", f" -> ⚠️ Pacing Intervention: Removed redundant chapter '{removed['title']}'.")
elif pacing:
utils.log("ARCHITECT", f" -> Pacing OK. {pacing.get('reason', '')[:100]}")
# Increment loop
i += 1
@@ -254,7 +264,8 @@ def process_book(bp, folder, context="", resume=False, interactive=False):
prog = 15 + int((i / len(chapters)) * 75)
utils.update_progress(prog)
utils.log("TIMING", f" -> Chapter {ch['chapter_number']} finished in {duration:.1f}s | Avg: {avg_time:.1f}s | ETA: {int(eta//60)}m {int(eta%60)}s")
word_count = len(txt.split()) if txt else 0
utils.log("TIMING", f" -> Ch {ch['chapter_number']} done in {duration:.1f}s | {word_count:,} words | Avg: {avg_time:.1f}s | ETA: {int(eta//60)}m {int(eta%60)}s")
utils.log("TIMING", f"Writing Phase: {time.time() - t_step:.1f}s")