v1.3.1: Remove rigidity from chapter counts, beats, word lengths, and bridge chapters
story.py — create_chapter_plan(): - TARGET_CHAPTERS is now a guideline (±15%) not a hard constraint; the AI can produce a count that fits the story rather than forcing a specific number - Word scaling is now pacing-aware instead of uniform: Very Fast ≈ 60% of avg, Fast ≈ 80%, Standard ≈ 100%, Slow ≈ 125%, Very Slow ≈ 150% - Two-pass normalisation: pacing weights applied first, then the total is nudged to the word target — natural variation preserved throughout - Variance range tightened to ±8% (was ±10%) for more predictable totals - Prompt now tells the AI that estimated_words should reflect pacing rhythm story.py — expand(): - Added event ceiling (target_chapters × 1.5): if the outline already has enough beats, the pass switches from "add events" to "enrich descriptions" — prevents over-dense outlines for short stories and flash fiction - Task instruction is dynamically chosen: add-events vs deepen-descriptions - Clarified that original user beats must be preserved but new events must each be distinct and spread evenly (not front-loaded) story.py — refinement loop: - Word count constraint softened from hard "do not condense" to "~N words ±20% acceptable if the scene demands it" so action chapters can run short and introspective chapters can run long naturally main.py — bridge chapter insertion: - Removed hardcoded 1500-word estimate for dynamically inserted bridge chapters; now computes the average estimated_words from the current chapter plan so bridge chapters match the book's natural chapter length Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -65,4 +65,4 @@ LENGTH_DEFINITIONS = {
|
||||
}
|
||||
|
||||
# --- SYSTEM ---
|
||||
VERSION = "1.3.0"
|
||||
VERSION = "1.3.1"
|
||||
7
main.py
7
main.py
@@ -213,12 +213,17 @@ def process_book(bp, folder, context="", resume=False, interactive=False):
|
||||
pacing = story.check_pacing(bp, summary, txt, ch, remaining, folder)
|
||||
if pacing and pacing.get('status') == 'add_bridge':
|
||||
new_data = pacing.get('new_chapter', {})
|
||||
# Estimate bridge chapter length from current plan average (not hardcoded)
|
||||
if chapters:
|
||||
avg_words = int(sum(c.get('estimated_words', 1500) for c in chapters) / len(chapters))
|
||||
else:
|
||||
avg_words = 1500
|
||||
new_ch = {
|
||||
"chapter_number": ch['chapter_number'] + 1,
|
||||
"title": new_data.get('title', 'Bridge Chapter'),
|
||||
"pov_character": new_data.get('pov_character', ch.get('pov_character')),
|
||||
"pacing": "Slow",
|
||||
"estimated_words": 1500,
|
||||
"estimated_words": avg_words,
|
||||
"beats": new_data.get('beats', [])
|
||||
}
|
||||
chapters.insert(i+1, new_ch)
|
||||
|
||||
@@ -261,27 +261,38 @@ def plan_structure(bp, folder):
|
||||
def expand(events, pass_num, target_chapters, bp, folder):
|
||||
utils.log("ARCHITECT", f"Expansion pass {pass_num} | Current Beats: {len(events)} | Target Chaps: {target_chapters}")
|
||||
|
||||
beats_context = []
|
||||
# If events already well exceed the target, only deepen descriptions — don't add more
|
||||
event_ceiling = int(target_chapters * 1.5)
|
||||
if len(events) >= event_ceiling:
|
||||
task = (
|
||||
f"The outline already has {len(events)} beats for a {target_chapters}-chapter book — do NOT add more events. "
|
||||
f"Instead, enrich each existing beat's description with more specific detail: setting, characters involved, emotional stakes, and how it connects to what follows."
|
||||
)
|
||||
else:
|
||||
task = (
|
||||
f"Expand the outline toward {target_chapters} chapters. "
|
||||
f"Current count: {len(events)} beats. "
|
||||
f"Add intermediate events to fill pacing gaps, deepen subplots, and ensure character arcs are visible. "
|
||||
f"Do not overshoot — aim for {target_chapters} to {event_ceiling} total events."
|
||||
)
|
||||
|
||||
if not beats_context:
|
||||
beats_context = bp.get('plot_beats', [])
|
||||
original_beats = bp.get('plot_beats', [])
|
||||
|
||||
prompt = f"""
|
||||
ROLE: Story Architect
|
||||
TASK: Expand the outline to fit a {target_chapters}-chapter book.
|
||||
CURRENT_COUNT: {len(events)} beats.
|
||||
TASK: {task}
|
||||
|
||||
INPUT_OUTLINE:
|
||||
{json.dumps(beats_context)}
|
||||
ORIGINAL_USER_BEATS (must all remain present):
|
||||
{json.dumps(original_beats)}
|
||||
|
||||
CURRENT_EVENTS:
|
||||
{json.dumps(events)}
|
||||
|
||||
RULES:
|
||||
1. Detect pacing gaps.
|
||||
2. Insert intermediate events.
|
||||
3. Deepen subplots.
|
||||
4. PRESERVE original beats.
|
||||
1. PRESERVE all original user beats — do not remove or alter them.
|
||||
2. New events must serve a clear narrative purpose (tension, character, world, reversal).
|
||||
3. Avoid repetitive events — each beat must be distinct.
|
||||
4. Distribute additions evenly — do not front-load the outline.
|
||||
|
||||
OUTPUT_FORMAT (JSON): {{ "events": [{{"description": "String", "purpose": "String"}}] }}
|
||||
"""
|
||||
@@ -322,12 +333,17 @@ def create_chapter_plan(events, bp, folder):
|
||||
|
||||
prompt = f"""
|
||||
ROLE: Pacing Specialist
|
||||
TASK: Group events into Chapters.
|
||||
TASK: Group the provided events into chapters for a {meta.get('genre', 'Fiction')} {bp['length_settings'].get('label', 'novel')}.
|
||||
|
||||
CONSTRAINTS:
|
||||
- TARGET_CHAPTERS: {target}
|
||||
- TARGET_WORDS: {words}
|
||||
- INSTRUCTIONS:
|
||||
GUIDELINES:
|
||||
- AIM for approximately {target} chapters, but the final count may vary ±15% if the story structure demands it.
|
||||
(e.g. a tightly plotted thriller may need fewer; an epic with many subplots may need more.)
|
||||
- TARGET_WORDS for the whole book: {words}
|
||||
- Assign pacing to each chapter: Very Fast / Fast / Standard / Slow / Very Slow
|
||||
Reflect dramatic rhythm — action scenes run fast, emotional beats run slow.
|
||||
- estimated_words per chapter should reflect its pacing:
|
||||
Very Fast ≈ 60% of average, Fast ≈ 80%, Standard ≈ 100%, Slow ≈ 125%, Very Slow ≈ 150%
|
||||
- Do NOT force equal word counts. Natural variation makes the book feel alive.
|
||||
{structure_instructions}
|
||||
{pov_instruction}
|
||||
|
||||
@@ -340,6 +356,7 @@ def create_chapter_plan(events, bp, folder):
|
||||
utils.log_usage(folder, ai.model_logic.name, response.usage_metadata)
|
||||
plan = json.loads(utils.clean_json(response.text))
|
||||
|
||||
# Parse target word count
|
||||
target_str = str(words).lower().replace(',', '').replace('k', '000').replace('+', '').replace(' ', '')
|
||||
target_val = 0
|
||||
if '-' in target_str:
|
||||
@@ -352,16 +369,31 @@ def create_chapter_plan(events, bp, folder):
|
||||
except: pass
|
||||
|
||||
if target_val > 0:
|
||||
variance = random.uniform(0.90, 1.10)
|
||||
variance = random.uniform(0.92, 1.08)
|
||||
target_val = int(target_val * variance)
|
||||
utils.log("ARCHITECT", f"Target adjusted with variance ({variance:.2f}x): {target_val} words.")
|
||||
utils.log("ARCHITECT", f"Word target after variance ({variance:.2f}x): {target_val} words.")
|
||||
|
||||
current_sum = sum(int(c.get('estimated_words', 0)) for c in plan)
|
||||
if current_sum > 0:
|
||||
factor = target_val / current_sum
|
||||
utils.log("ARCHITECT", f"Adjusting chapter lengths by {factor:.2f}x to match target.")
|
||||
base_factor = target_val / current_sum
|
||||
# Pacing multipliers — fast chapters are naturally shorter, slow chapters longer
|
||||
pacing_weight = {
|
||||
'very fast': 0.60, 'fast': 0.80, 'standard': 1.00,
|
||||
'slow': 1.25, 'very slow': 1.50
|
||||
}
|
||||
# Two-pass: apply pacing weights then normalise to hit total target
|
||||
for c in plan:
|
||||
c['estimated_words'] = int(c.get('estimated_words', 0) * factor)
|
||||
pw = pacing_weight.get(c.get('pacing', 'standard').lower(), 1.0)
|
||||
c['estimated_words'] = max(300, int(c.get('estimated_words', 0) * base_factor * pw))
|
||||
|
||||
# Normalise to keep total close to target
|
||||
adjusted_sum = sum(c['estimated_words'] for c in plan)
|
||||
if adjusted_sum > 0:
|
||||
norm = target_val / adjusted_sum
|
||||
for c in plan:
|
||||
c['estimated_words'] = max(300, int(c['estimated_words'] * norm))
|
||||
|
||||
utils.log("ARCHITECT", f"Chapter lengths scaled by pacing. Total ≈ {sum(c['estimated_words'] for c in plan)} words across {len(plan)} chapters.")
|
||||
|
||||
return plan
|
||||
except Exception as e:
|
||||
@@ -785,7 +817,7 @@ def write_chapter(chap, bp, folder, prev_sum, tracking=None, prev_content=None):
|
||||
{history_str}
|
||||
|
||||
HARD_CONSTRAINTS:
|
||||
- TARGET_WORDS: ~{est_words} (maintain this length — do not condense or summarise)
|
||||
- TARGET_WORDS: ~{est_words} words (aim for this; ±20% is acceptable if the scene genuinely demands it — but do not condense beats to save space)
|
||||
- BEATS MUST BE COVERED: {json.dumps(chap.get('beats', []))}
|
||||
- SUMMARY CONTEXT: {prev_sum[:1500]}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user