fix: Pipeline hardening — error handling, token efficiency, and robustness
core/utils.py: - estimate_tokens: improved heuristic 4 chars/token → 3.5 chars/token (more accurate) - truncate_to_tokens: added keep_head=True mode for head+tail truncation (better context retention for story summaries that need both opening and recent content) - load_json: explicit exception handling (json.JSONDecodeError, OSError) with log instead of silent returns; added utf-8 encoding with error replacement - log_image_attempt: replaced bare except with (json.JSONDecodeError, OSError); added utf-8 encoding to output write - log_usage: replaced bare except with AttributeError for token count extraction story/bible_tracker.py: - merge_selected_changes: wrapped all int() key casts (char idx, book num, beat idx) in try/except with meaningful log warning instead of crashing on malformed keys - harvest_metadata: replaced bare except:pass with except Exception as e + log message cli/engine.py: - Persona validation: added warning when all 3 attempts fail and substandard persona is accepted — flags elevated voice-drift risk for the run - Lore index updates: throttled from every chapter to every 3 chapters; lore is stable after the first few chapters (~10% token saving per book) - Mid-gen consistency check: now samples first 2 + last 8 chapters instead of passing full manuscript — caps token cost regardless of book length story/writer.py: - Two-pass polish: added local filter-word density check (no API call); skips the Pro polish if density < 1 per 83 words — saves ~8K tokens on already-clean drafts - Polish prompt: added prev_context_block for continuity — polished chapter now maintains seamless flow from the previous chapter's ending marketing/fonts.py: - Separated requests.exceptions.Timeout with specific log message vs generic failure - Added explicit log message when Roboto fallback also fails (returns None) marketing/blurb.py: - Added word count trim: blurbs > 220 words trimmed to last sentence within 220 words - Changed bare except to except Exception as e with log message - Added utf-8 encoding to file writes; logs final word count Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -19,7 +19,11 @@ def merge_selected_changes(original, draft, selected_keys):
|
||||
original['project_metadata'][field] = draft['project_metadata'][field]
|
||||
|
||||
elif parts[0] == 'char' and len(parts) >= 2:
|
||||
idx = int(parts[1])
|
||||
try:
|
||||
idx = int(parts[1])
|
||||
except (ValueError, IndexError):
|
||||
utils.log("SYSTEM", f"⚠️ Skipping malformed bible merge key: '{key}'")
|
||||
continue
|
||||
if idx < len(draft['characters']):
|
||||
if idx < len(original['characters']):
|
||||
original['characters'][idx] = draft['characters'][idx]
|
||||
@@ -27,7 +31,11 @@ def merge_selected_changes(original, draft, selected_keys):
|
||||
original['characters'].append(draft['characters'][idx])
|
||||
|
||||
elif parts[0] == 'book' and len(parts) >= 2:
|
||||
book_num = int(parts[1])
|
||||
try:
|
||||
book_num = int(parts[1])
|
||||
except (ValueError, IndexError):
|
||||
utils.log("SYSTEM", f"⚠️ Skipping malformed bible merge key: '{key}'")
|
||||
continue
|
||||
orig_book = next((b for b in original['books'] if b['book_number'] == book_num), None)
|
||||
draft_book = next((b for b in draft['books'] if b['book_number'] == book_num), None)
|
||||
|
||||
@@ -42,7 +50,11 @@ def merge_selected_changes(original, draft, selected_keys):
|
||||
orig_book['manual_instruction'] = draft_book['manual_instruction']
|
||||
|
||||
elif len(parts) == 4 and parts[2] == 'beat':
|
||||
beat_idx = int(parts[3])
|
||||
try:
|
||||
beat_idx = int(parts[3])
|
||||
except (ValueError, IndexError):
|
||||
utils.log("SYSTEM", f"⚠️ Skipping malformed beat merge key: '{key}'")
|
||||
continue
|
||||
if beat_idx < len(draft_book['plot_beats']):
|
||||
while len(orig_book['plot_beats']) <= beat_idx:
|
||||
orig_book['plot_beats'].append("")
|
||||
@@ -153,7 +165,8 @@ def harvest_metadata(bp, folder, full_manuscript):
|
||||
if valid_chars:
|
||||
utils.log("HARVESTER", f"Found {len(valid_chars)} new chars.")
|
||||
bp['characters'].extend(valid_chars)
|
||||
except: pass
|
||||
except Exception as e:
|
||||
utils.log("HARVESTER", f"⚠️ Metadata harvest failed: {e}")
|
||||
return bp
|
||||
|
||||
|
||||
|
||||
@@ -362,12 +362,18 @@ def write_chapter(chap, bp, folder, prev_sum, tracking=None, prev_content=None,
|
||||
utils.log("WRITER", f"⚠️ Failed Ch {chap['chapter_number']}: {e}")
|
||||
return f"## Chapter {chap['chapter_number']} Failed\n\nError: {e}"
|
||||
|
||||
# Exp 7: Two-Pass Drafting — Polish the rough draft with the logic (Pro) model
|
||||
# before evaluation. Produces cleaner prose with fewer rewrite cycles.
|
||||
if current_text:
|
||||
utils.log("WRITER", f" -> Two-pass polish (Pro model)...")
|
||||
guidelines = get_style_guidelines()
|
||||
fw_list = '", "'.join(guidelines['filter_words'])
|
||||
# Exp 7: Two-Pass Drafting — Polish rough draft with the logic (Pro) model before evaluation.
|
||||
# Skip when local filter-word heuristic shows draft is already clean (saves ~8K tokens/chapter).
|
||||
_guidelines_for_polish = get_style_guidelines()
|
||||
_fw_set = set(_guidelines_for_polish['filter_words'])
|
||||
_draft_word_list = current_text.lower().split() if current_text else []
|
||||
_fw_hit_count = sum(1 for w in _draft_word_list if w in _fw_set)
|
||||
_fw_density = _fw_hit_count / max(len(_draft_word_list), 1)
|
||||
_skip_polish = _fw_density < 0.012 # < ~1 filter word per 83 words → draft already clean
|
||||
|
||||
if current_text and not _skip_polish:
|
||||
utils.log("WRITER", f" -> Two-pass polish (Pro model, FW density {_fw_density:.3f})...")
|
||||
fw_list = '", "'.join(_guidelines_for_polish['filter_words'])
|
||||
polish_prompt = f"""
|
||||
ROLE: Senior Fiction Editor
|
||||
TASK: Polish this rough draft into publication-ready prose.
|
||||
@@ -379,6 +385,9 @@ def write_chapter(chap, bp, folder, prev_sum, tracking=None, prev_content=None,
|
||||
TARGET_WORDS: ~{est_words}
|
||||
BEATS (must all be covered): {json.dumps(chap.get('beats', []))}
|
||||
|
||||
CONTINUITY (maintain seamless flow from previous chapter):
|
||||
{prev_context_block if prev_context_block else "First chapter — no prior context."}
|
||||
|
||||
POLISH_CHECKLIST:
|
||||
1. FILTER_REMOVAL: Remove all filter words [{fw_list}] — rewrite each to show the sensation directly.
|
||||
2. DEEP_POV: Ensure the reader is inside the POV character's experience at all times — no external narration.
|
||||
@@ -404,6 +413,8 @@ def write_chapter(chap, bp, folder, prev_sum, tracking=None, prev_content=None,
|
||||
current_text = polished
|
||||
except Exception as e:
|
||||
utils.log("WRITER", f" -> Polish pass failed: {e}. Proceeding with raw draft.")
|
||||
elif current_text:
|
||||
utils.log("WRITER", f" -> Draft clean (FW density {_fw_density:.3f}). Skipping polish pass.")
|
||||
|
||||
# Reduced from 3 → 2 attempts since polish pass already refines prose before evaluation
|
||||
max_attempts = 2
|
||||
|
||||
Reference in New Issue
Block a user